From f4eeac20ccfb2e8ddc0bfdf1401d1a6b3e985691 Mon Sep 17 00:00:00 2001 From: Jamie Klassen Date: Tue, 5 Sep 2023 15:19:57 -0400 Subject: [PATCH 01/20] typecheck servicelocator mock Signed-off-by: Jamie Klassen --- .../service/KubernetesFanOutHandler.test.ts | 43 +++++++++++-------- 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.test.ts b/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.test.ts index 5dd74f6daa..a76406be07 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.test.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.test.ts @@ -20,6 +20,7 @@ import { CustomResource, ObjectFetchParams, KubernetesServiceLocator, + ServiceLocatorRequestContext, } from '../types/types'; import { KubernetesFanOutHandler } from './KubernetesFanOutHandler'; import { KubernetesClientBasedFetcher } from './KubernetesFetcher'; @@ -28,6 +29,7 @@ import { setupServer } from 'msw/node'; import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; import { ObjectsByEntityResponse } from '@backstage/plugin-kubernetes-common'; import { Config, ConfigReader } from '@backstage/config'; +import { Entity } from '@backstage/catalog-model'; describe('KubernetesFanOutHandler', () => { const fetchObjectsForService = jest.fn(); @@ -1054,24 +1056,29 @@ describe('KubernetesFanOutHandler', () => { ); const fleet: jest.Mocked = { - getClustersByEntity: jest.fn().mockResolvedValue({ - clusters: [ - { - name: 'works', - url: 'https://works', - authProvider: 'serviceAccount', - serviceAccountToken: 'token', - skipMetricsLookup: true, - }, - { - name: 'fails', - url: 'https://fails', - authProvider: 'serviceAccount', - serviceAccountToken: 'token', - skipMetricsLookup: true, - }, - ], - }), + getClustersByEntity: jest + .fn< + Promise<{ clusters: ClusterDetails[] }>, + [Entity, ServiceLocatorRequestContext] + >() + .mockResolvedValue({ + clusters: [ + { + name: 'works', + url: 'https://works', + authProvider: 'serviceAccount', + skipMetricsLookup: true, + serviceAccountToken: 'token', + }, + { + name: 'fails', + url: 'https://fails', + authProvider: 'serviceAccount', + skipMetricsLookup: true, + serviceAccountToken: 'token', + }, + ], + }), }; const logger = getVoidLogger(); const kubernetesFanOutHandler = new KubernetesFanOutHandler({ From e7b4146df8cc02494eabcb134fd16806011c4fd4 Mon Sep 17 00:00:00 2001 From: Jamie Klassen Date: Tue, 5 Sep 2023 15:24:11 -0400 Subject: [PATCH 02/20] typecheck expected value Signed-off-by: Jamie Klassen --- .../cluster-locator/CatalogClusterLocator.test.ts | 5 +++-- .../cluster-locator/ConfigClusterLocator.test.ts | 13 +++++++------ .../src/cluster-locator/index.test.ts | 3 ++- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.test.ts b/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.test.ts index f436024131..6fb337975e 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.test.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.test.ts @@ -17,6 +17,7 @@ import '@backstage/backend-common'; import { CatalogClusterLocator } from './CatalogClusterLocator'; import { CatalogApi } from '@backstage/catalog-client'; +import { ClusterDetails } from '../types/types'; const mockCatalogApi = { getEntityByRef: jest.fn(), @@ -86,7 +87,7 @@ describe('CatalogClusterLocator', () => { const result = await clusterSupplier.getClusters(); expect(result).toHaveLength(2); - expect(result[0]).toStrictEqual({ + expect(result[0]).toStrictEqual({ name: 'owned', url: 'https://apiserver.com', caData: 'caData', @@ -105,7 +106,7 @@ describe('CatalogClusterLocator', () => { const result = await clusterSupplier.getClusters(); expect(result).toHaveLength(2); - expect(result[1]).toStrictEqual({ + expect(result[1]).toStrictEqual({ name: 'owned', url: 'https://apiserver.com', caData: 'caData', diff --git a/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.test.ts b/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.test.ts index d84d7e39e8..b85dfac38e 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.test.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.test.ts @@ -17,6 +17,7 @@ import '@backstage/backend-common'; import { ConfigReader, Config } from '@backstage/config'; import { ConfigClusterLocator } from './ConfigClusterLocator'; +import { ClusterDetails } from '../types/types'; describe('ConfigClusterLocator', () => { it('empty clusters returns empty cluster details', async () => { @@ -46,7 +47,7 @@ describe('ConfigClusterLocator', () => { const result = await sut.getClusters(); - expect(result).toStrictEqual([ + expect(result).toStrictEqual([ { name: 'cluster1', serviceAccountToken: undefined, @@ -86,7 +87,7 @@ describe('ConfigClusterLocator', () => { const result = await sut.getClusters(); - expect(result).toStrictEqual([ + expect(result).toStrictEqual([ { name: 'cluster1', dashboardUrl: 'https://k8s.foo.com', @@ -143,7 +144,7 @@ describe('ConfigClusterLocator', () => { const result = await sut.getClusters(); - expect(result).toStrictEqual([ + expect(result).toStrictEqual([ { assumeRole: undefined, name: 'cluster1', @@ -204,7 +205,7 @@ describe('ConfigClusterLocator', () => { const result = await sut.getClusters(); - expect(result).toStrictEqual([ + expect(result).toStrictEqual([ { name: 'cluster1', serviceAccountToken: undefined, @@ -241,7 +242,7 @@ describe('ConfigClusterLocator', () => { const result = await sut.getClusters(); - expect(result).toStrictEqual([ + expect(result).toStrictEqual([ { name: 'cluster1', serviceAccountToken: undefined, @@ -294,7 +295,7 @@ describe('ConfigClusterLocator', () => { const result = await sut.getClusters(); - expect(result).toStrictEqual([ + expect(result).toStrictEqual([ { name: 'cluster1', serviceAccountToken: undefined, diff --git a/plugins/kubernetes-backend/src/cluster-locator/index.test.ts b/plugins/kubernetes-backend/src/cluster-locator/index.test.ts index 6847b0e0a3..1797d3a1b2 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/index.test.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/index.test.ts @@ -17,6 +17,7 @@ import { Config, ConfigReader } from '@backstage/config'; import { getCombinedClusterSupplier } from './index'; import { CatalogApi } from '@backstage/catalog-client'; +import { ClusterDetails } from '../types/types'; describe('getCombinedClusterSupplier', () => { let catalogApi: CatalogApi; @@ -51,7 +52,7 @@ describe('getCombinedClusterSupplier', () => { const clusterSupplier = getCombinedClusterSupplier(config, catalogApi); const result = await clusterSupplier.getClusters(); - expect(result).toStrictEqual([ + expect(result).toStrictEqual([ { name: 'cluster1', serviceAccountToken: 'token', From 094b12b59ea6c2341c25fc535ae8cc9d8ce7b5bf Mon Sep 17 00:00:00 2001 From: Jamie Klassen Date: Tue, 5 Sep 2023 15:42:34 -0400 Subject: [PATCH 03/20] refactor: migrate token field to authMetadata and remove the special ServiceAccountClusterDetails accordingly. Signed-off-by: Jamie Klassen --- plugins/kubernetes-backend/api-report.md | 17 ++++------- .../ConfigClusterLocator.test.ts | 15 +++------- .../cluster-locator/ConfigClusterLocator.ts | 3 +- .../src/cluster-locator/index.test.ts | 3 +- .../AksKubernetesAuthTranslator.test.ts | 2 +- .../AksKubernetesAuthTranslator.ts | 10 ++++++- .../AwsIamKubernetesAuthTranslator.test.ts | 6 ++-- .../AwsIamKubernetesAuthTranslator.ts | 13 +++++---- ...reIdentityKubernetesAuthTranslator.test.ts | 22 +++++++-------- .../AzureIdentityKubernetesAuthTranslator.ts | 5 +++- ...ispatchingKubernetesAuthTranslator.test.ts | 2 +- .../GoogleKubernetesAuthTranslator.ts | 5 +++- .../GoogleServiceAccountAuthProvider.ts | 5 +++- .../NoopKubernetesAuthTranslator.ts | 6 ++-- .../OidcKubernetesAuthTranslator.test.ts | 2 +- .../OidcKubernetesAuthTranslator.ts | 5 +++- .../src/service/KubernetesBuilder.test.ts | 2 -- .../service/KubernetesFanOutHandler.test.ts | 4 +-- .../src/service/KubernetesFetcher.test.ts | 28 +++++++++---------- .../src/service/KubernetesFetcher.ts | 4 +-- .../src/service/KubernetesProxy.test.ts | 19 ++++--------- .../src/service/KubernetesProxy.ts | 10 +++---- plugins/kubernetes-backend/src/types/types.ts | 15 ++-------- 23 files changed, 97 insertions(+), 106 deletions(-) diff --git a/plugins/kubernetes-backend/api-report.md b/plugins/kubernetes-backend/api-report.md index ad8ff32891..95aff769e9 100644 --- a/plugins/kubernetes-backend/api-report.md +++ b/plugins/kubernetes-backend/api-report.md @@ -65,6 +65,8 @@ export class AzureIdentityKubernetesAuthTranslator // @public (undocumented) export interface ClusterDetails { + // (undocumented) + authMetadata?: Record; // (undocumented) authProvider: string; // (undocumented) @@ -77,8 +79,6 @@ export interface ClusterDetails { dashboardUrl?: string; name: string; oidcTokenProvider?: string | undefined; - // (undocumented) - serviceAccountToken?: string | undefined; skipMetricsLookup?: boolean; // (undocumented) skipTLSVerify?: boolean; @@ -399,18 +399,14 @@ export interface KubernetesServiceLocator { export class NoopKubernetesAuthTranslator implements KubernetesAuthTranslator { // (undocumented) decorateClusterDetailsWithAuth( - clusterDetails: ServiceAccountClusterDetails, - ): Promise; + clusterDetails: ClusterDetails, + ): Promise; } // @public (undocumented) export interface ObjectFetchParams { // (undocumented) - clusterDetails: - | AWSClusterDetails - | GKEClusterDetails - | ServiceAccountClusterDetails - | ClusterDetails; + clusterDetails: AWSClusterDetails | GKEClusterDetails | ClusterDetails; // (undocumented) customResources: CustomResource[]; // (undocumented) @@ -463,9 +459,6 @@ export interface RouterOptions { permissions: PermissionEvaluator; } -// @public (undocumented) -export interface ServiceAccountClusterDetails extends ClusterDetails {} - // @public (undocumented) export type ServiceLocatorMethod = 'multiTenant' | 'http'; diff --git a/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.test.ts b/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.test.ts index b85dfac38e..f5d056f355 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.test.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.test.ts @@ -17,7 +17,7 @@ import '@backstage/backend-common'; import { ConfigReader, Config } from '@backstage/config'; import { ConfigClusterLocator } from './ConfigClusterLocator'; -import { ClusterDetails } from '../types/types'; +import { AWSClusterDetails, ClusterDetails } from '../types/types'; describe('ConfigClusterLocator', () => { it('empty clusters returns empty cluster details', async () => { @@ -50,7 +50,6 @@ describe('ConfigClusterLocator', () => { expect(result).toStrictEqual([ { name: 'cluster1', - serviceAccountToken: undefined, url: 'http://localhost:8080', authProvider: 'serviceAccount', skipMetricsLookup: false, @@ -91,17 +90,16 @@ describe('ConfigClusterLocator', () => { { name: 'cluster1', dashboardUrl: 'https://k8s.foo.com', - serviceAccountToken: 'token', url: 'http://localhost:8080', authProvider: 'serviceAccount', skipTLSVerify: false, skipMetricsLookup: true, caData: undefined, caFile: undefined, + authMetadata: { serviceAccountToken: 'token' }, }, { name: 'cluster2', - serviceAccountToken: undefined, url: 'http://localhost:8081', authProvider: 'google', skipTLSVerify: true, @@ -144,14 +142,14 @@ describe('ConfigClusterLocator', () => { const result = await sut.getClusters(); - expect(result).toStrictEqual([ + expect(result).toStrictEqual([ { assumeRole: undefined, name: 'cluster1', - serviceAccountToken: 'token', externalId: undefined, url: 'http://localhost:8080', authProvider: 'aws', + authMetadata: { serviceAccountToken: 'token' }, skipTLSVerify: false, skipMetricsLookup: false, caData: undefined, @@ -161,7 +159,6 @@ describe('ConfigClusterLocator', () => { assumeRole: 'SomeRole', name: 'cluster2', externalId: undefined, - serviceAccountToken: undefined, url: 'http://localhost:8081', authProvider: 'aws', skipTLSVerify: true, @@ -174,7 +171,6 @@ describe('ConfigClusterLocator', () => { name: 'cluster2', externalId: 'SomeExternalId', url: 'http://localhost:8081', - serviceAccountToken: undefined, authProvider: 'aws', skipTLSVerify: true, skipMetricsLookup: false, @@ -208,7 +204,6 @@ describe('ConfigClusterLocator', () => { expect(result).toStrictEqual([ { name: 'cluster1', - serviceAccountToken: undefined, url: 'http://localhost:8080', authProvider: 'serviceAccount', skipMetricsLookup: false, @@ -245,7 +240,6 @@ describe('ConfigClusterLocator', () => { expect(result).toStrictEqual([ { name: 'cluster1', - serviceAccountToken: undefined, url: 'http://localhost:8080', authProvider: 'serviceAccount', skipMetricsLookup: false, @@ -298,7 +292,6 @@ describe('ConfigClusterLocator', () => { expect(result).toStrictEqual([ { name: 'cluster1', - serviceAccountToken: undefined, url: 'http://localhost:8080', authProvider: 'serviceAccount', skipMetricsLookup: false, diff --git a/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.ts b/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.ts index 4d49e2d128..e83bd763c4 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.ts @@ -30,15 +30,16 @@ export class ConfigClusterLocator implements KubernetesClustersSupplier { return new ConfigClusterLocator( config.getConfigArray('clusters').map(c => { const authProvider = c.getString('authProvider'); + const serviceAccountToken = c.getOptionalString('serviceAccountToken'); const clusterDetails: ClusterDetails = { name: c.getString('name'), url: c.getString('url'), - serviceAccountToken: c.getOptionalString('serviceAccountToken'), skipTLSVerify: c.getOptionalBoolean('skipTLSVerify') ?? false, skipMetricsLookup: c.getOptionalBoolean('skipMetricsLookup') ?? false, caData: c.getOptionalString('caData'), caFile: c.getOptionalString('caFile'), authProvider: authProvider, + ...(serviceAccountToken && { authMetadata: { serviceAccountToken } }), }; const customResources = c.getOptionalConfigArray('customResources'); diff --git a/plugins/kubernetes-backend/src/cluster-locator/index.test.ts b/plugins/kubernetes-backend/src/cluster-locator/index.test.ts index 1797d3a1b2..e1bca888b5 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/index.test.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/index.test.ts @@ -55,9 +55,9 @@ describe('getCombinedClusterSupplier', () => { expect(result).toStrictEqual([ { name: 'cluster1', - serviceAccountToken: 'token', url: 'http://localhost:8080', authProvider: 'serviceAccount', + authMetadata: { serviceAccountToken: 'token' }, skipMetricsLookup: false, skipTLSVerify: false, caData: undefined, @@ -65,7 +65,6 @@ describe('getCombinedClusterSupplier', () => { }, { name: 'cluster2', - serviceAccountToken: undefined, url: 'http://localhost:8081', authProvider: 'google', skipMetricsLookup: false, diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/AksKubernetesAuthTranslator.test.ts b/plugins/kubernetes-backend/src/kubernetes-auth-translator/AksKubernetesAuthTranslator.test.ts index d0d3cf87bb..b39377f5a3 100644 --- a/plugins/kubernetes-backend/src/kubernetes-auth-translator/AksKubernetesAuthTranslator.test.ts +++ b/plugins/kubernetes-backend/src/kubernetes-auth-translator/AksKubernetesAuthTranslator.test.ts @@ -24,6 +24,6 @@ describe('AksKubernetesAuthTranslator', () => { { aks: 'aksToken' }, ); - expect(details.serviceAccountToken).toBe('aksToken'); + expect(details.authMetadata!.serviceAccountToken).toBe('aksToken'); }); }); diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/AksKubernetesAuthTranslator.ts b/plugins/kubernetes-backend/src/kubernetes-auth-translator/AksKubernetesAuthTranslator.ts index 13d784293e..bdaafc507b 100644 --- a/plugins/kubernetes-backend/src/kubernetes-auth-translator/AksKubernetesAuthTranslator.ts +++ b/plugins/kubernetes-backend/src/kubernetes-auth-translator/AksKubernetesAuthTranslator.ts @@ -25,6 +25,14 @@ export class AksKubernetesAuthTranslator { clusterDetails: ClusterDetails, auth: KubernetesRequestAuth, ): Promise { - return { ...clusterDetails, serviceAccountToken: auth.aks }; + return { + ...clusterDetails, + ...(auth.aks && { + authMetadata: { + serviceAccountToken: auth.aks, + ...clusterDetails.authMetadata, + }, + }), + }; } } diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.test.ts b/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.test.ts index b080aa332b..30bb240a9f 100644 --- a/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.test.ts +++ b/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.test.ts @@ -61,7 +61,7 @@ describe('AwsIamKubernetesAuthTranslator tests', () => { url: '', authProvider: 'aws', }); - expect((await authPromise).serviceAccountToken).toEqual( + expect((await authPromise).authMetadata!.serviceAccountToken).toEqual( 'k8s-aws-v1.aHR0cHM6Ly9odHRwczovL2V4YW1wbGUuY29tL2FzZGY_', ); }); @@ -75,7 +75,7 @@ describe('AwsIamKubernetesAuthTranslator tests', () => { url: '', authProvider: 'aws', }); - expect((await authPromise).serviceAccountToken).toEqual( + expect((await authPromise).authMetadata!.serviceAccountToken).toEqual( 'k8s-aws-v1.aHR0cHM6Ly9odHRwczovL2V4YW1wbGUuY29tL2FzZGY_', ); expect(fromTemporaryCredentials).toHaveBeenCalledWith({ @@ -102,7 +102,7 @@ describe('AwsIamKubernetesAuthTranslator tests', () => { url: '', authProvider: 'aws', }); - expect((await authPromise).serviceAccountToken).toEqual( + expect((await authPromise).authMetadata!.serviceAccountToken).toEqual( 'k8s-aws-v1.aHR0cHM6Ly9odHRwczovL2V4YW1wbGUuY29tL2FzZGY_', ); expect(fromTemporaryCredentials).toHaveBeenCalledWith({ diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.ts b/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.ts index cc46fd567a..c30cfd5bfd 100644 --- a/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.ts +++ b/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.ts @@ -117,11 +117,14 @@ export class AwsIamKubernetesAuthTranslator clusterDetails, ); - clusterDetailsWithAuthToken.serviceAccountToken = await this.getBearerToken( - clusterDetails.name, - clusterDetails.assumeRole, - clusterDetails.externalId, - ); + clusterDetailsWithAuthToken.authMetadata = { + serviceAccountToken: await this.getBearerToken( + clusterDetails.name, + clusterDetails.assumeRole, + clusterDetails.externalId, + ), + ...clusterDetailsWithAuthToken.authMetadata, + }; return clusterDetailsWithAuthToken; } } diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/AzureIdentityKubernetesAuthTranslator.test.ts b/plugins/kubernetes-backend/src/kubernetes-auth-translator/AzureIdentityKubernetesAuthTranslator.test.ts index 704551eb9f..dbc48857f4 100644 --- a/plugins/kubernetes-backend/src/kubernetes-auth-translator/AzureIdentityKubernetesAuthTranslator.test.ts +++ b/plugins/kubernetes-backend/src/kubernetes-auth-translator/AzureIdentityKubernetesAuthTranslator.test.ts @@ -57,7 +57,7 @@ describe('AzureIdentityKubernetesAuthTranslator tests', () => { ); const response = await authTranslator.decorateClusterDetailsWithAuth(cd); - expect(response.serviceAccountToken).toEqual('MY_TOKEN_1'); + expect(response.authMetadata!.serviceAccountToken).toEqual('MY_TOKEN_1'); }); it('should re-use token before expiry', async () => { @@ -67,10 +67,10 @@ describe('AzureIdentityKubernetesAuthTranslator tests', () => { ); const response = await authTranslator.decorateClusterDetailsWithAuth(cd); - expect(response.serviceAccountToken).toEqual('MY_TOKEN_1'); + expect(response.authMetadata!.serviceAccountToken).toEqual('MY_TOKEN_1'); const response2 = await authTranslator.decorateClusterDetailsWithAuth(cd); - expect(response2.serviceAccountToken).toEqual('MY_TOKEN_1'); + expect(response2.authMetadata!.serviceAccountToken).toEqual('MY_TOKEN_1'); }); it('should issue new token 15 minutes befory expiry', async () => { @@ -82,12 +82,12 @@ describe('AzureIdentityKubernetesAuthTranslator tests', () => { ); const response = await authTranslator.decorateClusterDetailsWithAuth(cd); - expect(response.serviceAccountToken).toEqual('MY_TOKEN_1'); + expect(response.authMetadata!.serviceAccountToken).toEqual('MY_TOKEN_1'); jest.setSystemTime(Date.now() + 2 * 60 * 1000); // advance time by 2mins const response2 = await authTranslator.decorateClusterDetailsWithAuth(cd); - expect(response2.serviceAccountToken).toEqual('MY_TOKEN_2'); + expect(response2.authMetadata!.serviceAccountToken).toEqual('MY_TOKEN_2'); }); it('should re-use existing token if there is afailure', async () => { @@ -99,20 +99,20 @@ describe('AzureIdentityKubernetesAuthTranslator tests', () => { ); const response = await authTranslator.decorateClusterDetailsWithAuth(cd); - expect(response.serviceAccountToken).toEqual('MY_TOKEN_1'); + expect(response.authMetadata!.serviceAccountToken).toEqual('MY_TOKEN_1'); jest.setSystemTime(Date.now() + 2 * 60 * 1000); // advance time by 2min const response2 = await authTranslator.decorateClusterDetailsWithAuth(cd); - expect(response2.serviceAccountToken).toEqual('MY_TOKEN_2'); + expect(response2.authMetadata!.serviceAccountToken).toEqual('MY_TOKEN_2'); jest.setSystemTime(Date.now() + 2 * 60 * 1000); // advance time by 2min const response3 = await authTranslator.decorateClusterDetailsWithAuth(cd); - expect(response3.serviceAccountToken).toEqual('MY_TOKEN_2'); + expect(response3.authMetadata!.serviceAccountToken).toEqual('MY_TOKEN_2'); const response4 = await authTranslator.decorateClusterDetailsWithAuth(cd); - expect(response4.serviceAccountToken).toEqual('MY_TOKEN_4'); + expect(response4.authMetadata!.serviceAccountToken).toEqual('MY_TOKEN_4'); }); it('should throw if existing token expired and failed to fetch a new one', async () => { @@ -124,12 +124,12 @@ describe('AzureIdentityKubernetesAuthTranslator tests', () => { ); const response = await authTranslator.decorateClusterDetailsWithAuth(cd); - expect(response.serviceAccountToken).toEqual('MY_TOKEN_1'); + expect(response.authMetadata!.serviceAccountToken).toEqual('MY_TOKEN_1'); jest.setSystemTime(Date.now() + 2 * 60 * 1000); // advance time by 2min const response2 = await authTranslator.decorateClusterDetailsWithAuth(cd); - expect(response2.serviceAccountToken).toEqual('MY_TOKEN_2'); + expect(response2.authMetadata!.serviceAccountToken).toEqual('MY_TOKEN_2'); jest.setSystemTime(Date.now() + 17 * 60 * 1000); // advance time by 17min diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/AzureIdentityKubernetesAuthTranslator.ts b/plugins/kubernetes-backend/src/kubernetes-auth-translator/AzureIdentityKubernetesAuthTranslator.ts index 2633598d6c..784e3c7311 100644 --- a/plugins/kubernetes-backend/src/kubernetes-auth-translator/AzureIdentityKubernetesAuthTranslator.ts +++ b/plugins/kubernetes-backend/src/kubernetes-auth-translator/AzureIdentityKubernetesAuthTranslator.ts @@ -48,7 +48,10 @@ export class AzureIdentityKubernetesAuthTranslator clusterDetails, ); - clusterDetailsWithAuthToken.serviceAccountToken = await this.getToken(); + clusterDetailsWithAuthToken.authMetadata = { + serviceAccountToken: await this.getToken(), + ...clusterDetailsWithAuthToken.authMetadata, + }; return clusterDetailsWithAuthToken; } diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/DispatchingKubernetesAuthTranslator.test.ts b/plugins/kubernetes-backend/src/kubernetes-auth-translator/DispatchingKubernetesAuthTranslator.test.ts index 3132bdef76..be41c593f2 100644 --- a/plugins/kubernetes-backend/src/kubernetes-auth-translator/DispatchingKubernetesAuthTranslator.test.ts +++ b/plugins/kubernetes-backend/src/kubernetes-auth-translator/DispatchingKubernetesAuthTranslator.test.ts @@ -36,7 +36,7 @@ describe('decorateClusterDetailsWithAuth', () => { url: 'notanything.com', name: 'randomName', authProvider: 'google', - serviceAccountToken: 'added by mock translator', + authMetadata: { serviceAccountToken: 'added by mock translator' }, }; mockTranslator.decorateClusterDetailsWithAuth.mockResolvedValue( diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/GoogleKubernetesAuthTranslator.ts b/plugins/kubernetes-backend/src/kubernetes-auth-translator/GoogleKubernetesAuthTranslator.ts index e8dae56997..96b2daebad 100644 --- a/plugins/kubernetes-backend/src/kubernetes-auth-translator/GoogleKubernetesAuthTranslator.ts +++ b/plugins/kubernetes-backend/src/kubernetes-auth-translator/GoogleKubernetesAuthTranslator.ts @@ -36,7 +36,10 @@ export class GoogleKubernetesAuthTranslator const authToken: string | undefined = authConfig.google; if (authToken) { - clusterDetailsWithAuthToken.serviceAccountToken = authToken; + clusterDetailsWithAuthToken.authMetadata = { + serviceAccountToken: authToken, + ...clusterDetailsWithAuthToken.authMetadata, + }; } else { throw new Error( 'Google token not found under auth.google in request body', diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/GoogleServiceAccountAuthProvider.ts b/plugins/kubernetes-backend/src/kubernetes-auth-translator/GoogleServiceAccountAuthProvider.ts index cca510ad6c..81b50c478b 100644 --- a/plugins/kubernetes-backend/src/kubernetes-auth-translator/GoogleServiceAccountAuthProvider.ts +++ b/plugins/kubernetes-backend/src/kubernetes-auth-translator/GoogleServiceAccountAuthProvider.ts @@ -35,7 +35,10 @@ export class GoogleServiceAccountAuthTranslator const accessToken = await client.auth.getAccessToken(); if (accessToken) { - clusterDetailsWithAuthToken.serviceAccountToken = accessToken; + clusterDetailsWithAuthToken.authMetadata = { + serviceAccountToken: accessToken, + ...clusterDetailsWithAuthToken.authMetadata, + }; } else { throw new Error( 'Unable to obtain access token for the current Google Application Default Credentials', diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/NoopKubernetesAuthTranslator.ts b/plugins/kubernetes-backend/src/kubernetes-auth-translator/NoopKubernetesAuthTranslator.ts index 32aa141e50..a44710f58b 100644 --- a/plugins/kubernetes-backend/src/kubernetes-auth-translator/NoopKubernetesAuthTranslator.ts +++ b/plugins/kubernetes-backend/src/kubernetes-auth-translator/NoopKubernetesAuthTranslator.ts @@ -15,7 +15,7 @@ */ import { KubernetesAuthTranslator } from './types'; -import { ServiceAccountClusterDetails } from '../types/types'; +import { ClusterDetails } from '../types/types'; /** * @@ -23,8 +23,8 @@ import { ServiceAccountClusterDetails } from '../types/types'; */ export class NoopKubernetesAuthTranslator implements KubernetesAuthTranslator { async decorateClusterDetailsWithAuth( - clusterDetails: ServiceAccountClusterDetails, - ): Promise { + clusterDetails: ClusterDetails, + ): Promise { return clusterDetails; } } diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/OidcKubernetesAuthTranslator.test.ts b/plugins/kubernetes-backend/src/kubernetes-auth-translator/OidcKubernetesAuthTranslator.test.ts index 68f24bd020..fe604008f1 100644 --- a/plugins/kubernetes-backend/src/kubernetes-auth-translator/OidcKubernetesAuthTranslator.test.ts +++ b/plugins/kubernetes-backend/src/kubernetes-auth-translator/OidcKubernetesAuthTranslator.test.ts @@ -36,7 +36,7 @@ describe('OidcKubernetesAuthTranslator tests', () => { }, ); - expect(details.serviceAccountToken).toBe('fakeToken'); + expect(details.authMetadata!.serviceAccountToken).toBe('fakeToken'); }); it('returns error when oidcTokenProvider is not configured', async () => { diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/OidcKubernetesAuthTranslator.ts b/plugins/kubernetes-backend/src/kubernetes-auth-translator/OidcKubernetesAuthTranslator.ts index cee1ec1e0c..5c0c0de181 100644 --- a/plugins/kubernetes-backend/src/kubernetes-auth-translator/OidcKubernetesAuthTranslator.ts +++ b/plugins/kubernetes-backend/src/kubernetes-auth-translator/OidcKubernetesAuthTranslator.ts @@ -43,7 +43,10 @@ export class OidcKubernetesAuthTranslator implements KubernetesAuthTranslator { const authToken: string | undefined = authConfig.oidc?.[oidcTokenProvider]; if (authToken) { - clusterDetailsWithAuthToken.serviceAccountToken = authToken; + clusterDetailsWithAuthToken.authMetadata = { + serviceAccountToken: authToken, + ...clusterDetailsWithAuthToken.authMetadata, + }; } else { throw new Error( `Auth token not found under oidc.${oidcTokenProvider} in request body`, diff --git a/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts b/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts index 8eb7a066df..9d663a4185 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts @@ -64,7 +64,6 @@ describe('KubernetesBuilder', () => { name: 'some-cluster', authProvider: 'serviceAccount', url: 'https://localhost:1234', - serviceAccountToken: 'someToken', }, { name: 'some-other-cluster', @@ -192,7 +191,6 @@ describe('KubernetesBuilder', () => { name: 'some-cluster', authProvider: 'serviceAccount', url: 'https://localhost:1234', - serviceAccountToken: 'someToken', }; const clusters: ClusterDetails[] = [ someCluster, diff --git a/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.test.ts b/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.test.ts index a76406be07..e092ac082c 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.test.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.test.ts @@ -1068,14 +1068,14 @@ describe('KubernetesFanOutHandler', () => { url: 'https://works', authProvider: 'serviceAccount', skipMetricsLookup: true, - serviceAccountToken: 'token', + authMetadata: { serviceAccountToken: 'token' }, }, { name: 'fails', url: 'https://fails', authProvider: 'serviceAccount', skipMetricsLookup: true, - serviceAccountToken: 'token', + authMetadata: { serviceAccountToken: 'token' }, }, ], }), diff --git a/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts b/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts index 1a603a7a98..37f134be46 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts @@ -138,8 +138,8 @@ describe('KubernetesFetcher', () => { clusterDetails: { name: 'cluster1', url: 'http://localhost:9999', - serviceAccountToken: 'token', authProvider: 'serviceAccount', + authMetadata: { serviceAccountToken: 'token' }, }, objectTypesToFetch: OBJECTS_TO_FETCH, labelSelector: '', @@ -199,8 +199,8 @@ describe('KubernetesFetcher', () => { clusterDetails: { name: 'cluster1', url: 'http://localhost:9999/k8s/clusters/1234', - serviceAccountToken: 'token', authProvider: 'serviceAccount', + authMetadata: { serviceAccountToken: 'token' }, }, objectTypesToFetch: OBJECTS_TO_FETCH, labelSelector: '', @@ -309,8 +309,8 @@ describe('KubernetesFetcher', () => { clusterDetails: { name: 'cluster1', url: 'http://localhost:9999', - serviceAccountToken: 'token', authProvider: 'serviceAccount', + authMetadata: { serviceAccountToken: 'token' }, }, objectTypesToFetch: OBJECTS_TO_FETCH, labelSelector: '', @@ -383,8 +383,8 @@ describe('KubernetesFetcher', () => { clusterDetails: { name: 'cluster1', url: 'http://localhost:9999', - serviceAccountToken: 'token', authProvider: 'serviceAccount', + authMetadata: { serviceAccountToken: 'token' }, }, objectTypesToFetch: OBJECTS_TO_FETCH, labelSelector: '', @@ -475,8 +475,8 @@ describe('KubernetesFetcher', () => { clusterDetails: { name: 'cluster1', url: 'http://localhost:9999', - serviceAccountToken: 'token', authProvider: 'serviceAccount', + authMetadata: { serviceAccountToken: 'token' }, }, objectTypesToFetch: OBJECTS_TO_FETCH, labelSelector: '', @@ -563,8 +563,8 @@ describe('KubernetesFetcher', () => { clusterDetails: { name: 'cluster1', url: 'http://badurl.does.not.exist', - serviceAccountToken: 'token', authProvider: 'serviceAccount', + authMetadata: { serviceAccountToken: 'token' }, }, objectTypesToFetch: OBJECTS_TO_FETCH, labelSelector: '', @@ -600,8 +600,8 @@ describe('KubernetesFetcher', () => { clusterDetails: { name: 'cluster1', url: 'http://localhost:9999', - serviceAccountToken: 'token', authProvider: 'serviceAccount', + authMetadata: { serviceAccountToken: 'token' }, }, objectTypesToFetch: OBJECTS_TO_FETCH, labelSelector: 'service-label=value', @@ -667,8 +667,8 @@ describe('KubernetesFetcher', () => { clusterDetails: { name: 'cluster1', url: 'https://localhost:9999', - serviceAccountToken: 'token', authProvider: 'serviceAccount', + authMetadata: { serviceAccountToken: 'token' }, caData: 'MOCKCA', }, objectTypesToFetch: new Set([ @@ -704,8 +704,8 @@ describe('KubernetesFetcher', () => { clusterDetails: { name: 'cluster1', url: 'https://localhost:9999', - serviceAccountToken: 'token', authProvider: 'serviceAccount', + authMetadata: { serviceAccountToken: 'token' }, }, objectTypesToFetch: new Set([ { @@ -747,8 +747,8 @@ describe('KubernetesFetcher', () => { clusterDetails: { name: 'cluster1', url: 'https://localhost:9999', - serviceAccountToken: 'token', authProvider: 'serviceAccount', + authMetadata: { serviceAccountToken: 'token' }, caFile: '/path/to/ca.crt', }, objectTypesToFetch: new Set([ @@ -785,8 +785,8 @@ describe('KubernetesFetcher', () => { clusterDetails: { name: 'cluster1', url: 'https://localhost:9999', - serviceAccountToken: 'token', authProvider: 'serviceAccount', + authMetadata: { serviceAccountToken: 'token' }, skipTLSVerify: true, }, objectTypesToFetch: new Set([ @@ -836,8 +836,8 @@ describe('KubernetesFetcher', () => { clusterDetails: { name: 'cluster1', url: 'http://localhost:9999', - serviceAccountToken: 'token', authProvider: 'serviceAccount', + authMetadata: { serviceAccountToken: 'token' }, }, objectTypesToFetch: OBJECTS_TO_FETCH, labelSelector: '', @@ -1019,8 +1019,8 @@ describe('KubernetesFetcher', () => { { name: 'cluster1', url: 'http://localhost:9999', - serviceAccountToken: 'token', authProvider: 'serviceAccount', + authMetadata: { serviceAccountToken: 'token' }, }, new Set(['ns-a']), ); @@ -1106,8 +1106,8 @@ describe('KubernetesFetcher', () => { { name: 'cluster1', url: 'http://localhost:9999', - serviceAccountToken: 'token', authProvider: 'serviceAccount', + authMetadata: { serviceAccountToken: 'token' }, }, new Set(['ns-a', 'ns-b']), ); diff --git a/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts b/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts index 25bade3e97..f3499f039e 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts @@ -201,7 +201,7 @@ export class KubernetesClientBasedFetcher implements KubernetesFetcher { let url: URL; let requestInit: RequestInit; if ( - clusterDetails.serviceAccountToken || + clusterDetails?.authMetadata?.serviceAccountToken || clusterDetails.authProvider === 'localKubectlProxy' ) { [url, requestInit] = this.fetchArgsFromClusterDetails(clusterDetails); @@ -236,7 +236,7 @@ export class KubernetesClientBasedFetcher implements KubernetesFetcher { headers: { Accept: 'application/json', 'Content-Type': 'application/json', - Authorization: `Bearer ${clusterDetails.serviceAccountToken}`, + Authorization: `Bearer ${clusterDetails.authMetadata?.serviceAccountToken}`, }, }; diff --git a/plugins/kubernetes-backend/src/service/KubernetesProxy.test.ts b/plugins/kubernetes-backend/src/service/KubernetesProxy.test.ts index 6c8953d0a3..8f25b217ab 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesProxy.test.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesProxy.test.ts @@ -154,8 +154,8 @@ describe('KubernetesProxy', () => { { name: 'cluster1', url: 'https://localhost:9999', - serviceAccountToken: 'tokenA', authProvider: 'googleServiceAccount', + authMetadata: { serviceAccountToken: 'tokenA' }, } as ClusterDetails, ]); @@ -172,8 +172,8 @@ describe('KubernetesProxy', () => { { name: 'cluster1', url: 'https://localhost:9999', - serviceAccountToken: 'tokenA', authProvider: 'googleServiceAccount', + authMetadata: { serviceAccountToken: 'tokenA' }, } as ClusterDetails, ]); @@ -201,7 +201,6 @@ describe('KubernetesProxy', () => { { name: 'cluster1', url: 'https://localhost:9999', - serviceAccountToken: '', authProvider: 'serviceAccount', }, ] as ClusterDetails[]); @@ -209,7 +208,6 @@ describe('KubernetesProxy', () => { authTranslator.decorateClusterDetailsWithAuth.mockResolvedValue({ name: 'cluster1', url: 'https://localhost:9999', - serviceAccountToken: '', authProvider: 'serviceAccount', } as ClusterDetails); @@ -247,7 +245,6 @@ describe('KubernetesProxy', () => { { name: 'cluster1', url: 'https://localhost:9999', - serviceAccountToken: '', authProvider: 'serviceAccount', }, ] as ClusterDetails[]); @@ -255,7 +252,6 @@ describe('KubernetesProxy', () => { authTranslator.decorateClusterDetailsWithAuth.mockResolvedValue({ name: 'cluster1', url: 'https://localhost:9999', - serviceAccountToken: '', authProvider: 'serviceAccount', } as ClusterDetails); @@ -344,7 +340,6 @@ describe('KubernetesProxy', () => { { name: 'cluster1', url: 'https://localhost:9999', - serviceAccountToken: '', authProvider: 'serviceAccount', }, ] as ClusterDetails[]); @@ -352,8 +347,8 @@ describe('KubernetesProxy', () => { authTranslator.decorateClusterDetailsWithAuth.mockResolvedValue({ name: 'cluster1', url: 'https://localhost:9999', - serviceAccountToken: 'translator-provided-token', authProvider: 'serviceAccount', + authMetadata: { serviceAccountToken: 'translator-provided-token' }, } as ClusterDetails); const requestPromise = setupProxyPromise({ @@ -401,8 +396,8 @@ describe('KubernetesProxy', () => { authTranslator.decorateClusterDetailsWithAuth.mockResolvedValue({ name: 'cluster1', url: 'https://localhost:9999', - serviceAccountToken: 'my-token', authProvider: 'googleServiceAccount', + authMetadata: { serviceAccountToken: 'my-token' }, } as ClusterDetails); const requestPromise = setupProxyPromise({ @@ -455,8 +450,8 @@ describe('KubernetesProxy', () => { authTranslator.decorateClusterDetailsWithAuth.mockResolvedValue({ name: 'cluster1', url: 'https://localhost:9999', - serviceAccountToken: 'tokenA', authProvider: 'googleServiceAccount', + authMetadata: { serviceAccountToken: 'tokenA' }, } as ClusterDetails); const requestPromise = setupProxyPromise({ @@ -600,7 +595,7 @@ describe('KubernetesProxy', () => { name: 'cluster1', url: 'https://localhost:9999', authProvider: 'google', - serviceAccountToken: 'client-side-token', + authMetadata: { serviceAccountToken: 'client-side-token' }, }, ] as ClusterDetails[]); @@ -788,7 +783,6 @@ describe('KubernetesProxy', () => { { name: 'local', url: `http://localhost:${wsPort}`, - serviceAccountToken: '', authProvider: 'serviceAccount', }, ] as ClusterDetails[]); @@ -796,7 +790,6 @@ describe('KubernetesProxy', () => { authTranslator.decorateClusterDetailsWithAuth.mockResolvedValue({ name: 'local', url: `http://localhost:${wsPort}`, - serviceAccountToken: '', authProvider: 'serviceAccount', } as ClusterDetails); diff --git a/plugins/kubernetes-backend/src/service/KubernetesProxy.ts b/plugins/kubernetes-backend/src/service/KubernetesProxy.ts index 8ae1fb9fcc..e3fc293436 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesProxy.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesProxy.ts @@ -112,11 +112,11 @@ export class KubernetesProxy { if (authHeader) { req.headers.authorization = authHeader; } else { - const { serviceAccountToken } = await this.getClusterForRequest( - req, - ).then(cd => - this.authTranslator.decorateClusterDetailsWithAuth(cd, {}), - ); + const serviceAccountToken = await this.getClusterForRequest(req) + .then(cd => + this.authTranslator.decorateClusterDetailsWithAuth(cd, {}), + ) + .then(cd => cd?.authMetadata?.serviceAccountToken); if (serviceAccountToken) { req.headers.authorization = `Bearer ${serviceAccountToken}`; } diff --git a/plugins/kubernetes-backend/src/types/types.ts b/plugins/kubernetes-backend/src/types/types.ts index 15ed1662de..6711462916 100644 --- a/plugins/kubernetes-backend/src/types/types.ts +++ b/plugins/kubernetes-backend/src/types/types.ts @@ -33,11 +33,7 @@ import { Config } from '@backstage/config'; */ export interface ObjectFetchParams { serviceId: string; - clusterDetails: - | AWSClusterDetails - | GKEClusterDetails - | ServiceAccountClusterDetails - | ClusterDetails; + clusterDetails: AWSClusterDetails | GKEClusterDetails | ClusterDetails; objectTypesToFetch: Set; labelSelector: string; customResources: CustomResource[]; @@ -159,7 +155,6 @@ export interface ClusterDetails { name: string; url: string; authProvider: string; - serviceAccountToken?: string | undefined; /** * oidc provider used to get id tokens to authenticate against kubernetes */ @@ -209,6 +204,8 @@ export interface ClusterDetails { * Kubernetes resources. */ customResources?: CustomResourceMatcher[]; + + authMetadata?: Record; } /** @@ -223,12 +220,6 @@ export interface GKEClusterDetails extends ClusterDetails {} */ export interface AzureClusterDetails extends ClusterDetails {} -/** - * - * @public - */ -export interface ServiceAccountClusterDetails extends ClusterDetails {} - /** * * @public From 79820945395e06523eea3cad591542a881c92943 Mon Sep 17 00:00:00 2001 From: Jamie Klassen Date: Tue, 5 Sep 2023 16:02:39 -0400 Subject: [PATCH 04/20] migrate AWS config to authMetadata and remove specialized AWSClusterDetails type accordingly Signed-off-by: Jamie Klassen --- plugins/kubernetes-backend/api-report.md | 14 +++------- .../CatalogClusterLocator.test.ts | 6 +++-- .../cluster-locator/CatalogClusterLocator.ts | 26 +++++++++---------- .../ConfigClusterLocator.test.ts | 21 ++++++++------- .../cluster-locator/ConfigClusterLocator.ts | 9 ++++++- .../AwsIamKubernetesAuthTranslator.test.ts | 8 +++--- .../AwsIamKubernetesAuthTranslator.ts | 12 ++++----- plugins/kubernetes-backend/src/types/types.ts | 11 +------- 8 files changed, 51 insertions(+), 56 deletions(-) diff --git a/plugins/kubernetes-backend/api-report.md b/plugins/kubernetes-backend/api-report.md index 95aff769e9..ebaae3dfa8 100644 --- a/plugins/kubernetes-backend/api-report.md +++ b/plugins/kubernetes-backend/api-report.md @@ -30,14 +30,6 @@ export class AksKubernetesAuthTranslator { ): Promise; } -// @public (undocumented) -export interface AWSClusterDetails extends ClusterDetails { - // (undocumented) - assumeRole?: string; - // (undocumented) - externalId?: string; -} - // @public (undocumented) export class AwsIamKubernetesAuthTranslator implements KubernetesAuthTranslator @@ -45,8 +37,8 @@ export class AwsIamKubernetesAuthTranslator constructor(opts: { config: Config }); // (undocumented) decorateClusterDetailsWithAuth( - clusterDetails: AWSClusterDetails, - ): Promise; + clusterDetails: ClusterDetails, + ): Promise; } // @public (undocumented) @@ -406,7 +398,7 @@ export class NoopKubernetesAuthTranslator implements KubernetesAuthTranslator { // @public (undocumented) export interface ObjectFetchParams { // (undocumented) - clusterDetails: AWSClusterDetails | GKEClusterDetails | ClusterDetails; + clusterDetails: GKEClusterDetails | ClusterDetails; // (undocumented) customResources: CustomResource[]; // (undocumented) diff --git a/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.test.ts b/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.test.ts index 6fb337975e..86776f4d02 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.test.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.test.ts @@ -111,8 +111,10 @@ describe('CatalogClusterLocator', () => { url: 'https://apiserver.com', caData: 'caData', authProvider: 'aws', - assumeRole: 'my-role', - externalId: 'my-id', + authMetadata: { + assumeRole: 'my-role', + externalId: 'my-id', + }, oidcTokenProvider: 'google', skipMetricsLookup: false, skipTLSVerify: false, diff --git a/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.ts b/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.ts index c48cc99eb6..9d81785a28 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.ts @@ -14,11 +14,7 @@ * limitations under the License. */ -import { - AWSClusterDetails, - ClusterDetails, - KubernetesClustersSupplier, -} from '../types/types'; +import { ClusterDetails, KubernetesClustersSupplier } from '../types/types'; import { CATALOG_FILTER_EXISTS, CatalogApi } from '@backstage/catalog-client'; import { ANNOTATION_KUBERNETES_API_SERVER, @@ -89,15 +85,17 @@ export class CatalogClusterLocator implements KubernetesClustersSupplier { if (clusterDetails.authProvider === 'aws') { return { ...clusterDetails, - assumeRole: - entity.metadata.annotations![ - ANNOTATION_KUBERNETES_AWS_ASSUME_ROLE - ]!, - externalId: - entity.metadata.annotations![ - ANNOTATION_KUBERNETES_AWS_EXTERNAL_ID - ]!, - } as AWSClusterDetails; + authMetadata: { + assumeRole: + entity.metadata.annotations![ + ANNOTATION_KUBERNETES_AWS_ASSUME_ROLE + ]!, + externalId: + entity.metadata.annotations![ + ANNOTATION_KUBERNETES_AWS_EXTERNAL_ID + ]!, + }, + }; } return clusterDetails; diff --git a/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.test.ts b/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.test.ts index f5d056f355..037a226476 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.test.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.test.ts @@ -17,7 +17,7 @@ import '@backstage/backend-common'; import { ConfigReader, Config } from '@backstage/config'; import { ConfigClusterLocator } from './ConfigClusterLocator'; -import { AWSClusterDetails, ClusterDetails } from '../types/types'; +import { ClusterDetails } from '../types/types'; describe('ConfigClusterLocator', () => { it('empty clusters returns empty cluster details', async () => { @@ -142,36 +142,39 @@ describe('ConfigClusterLocator', () => { const result = await sut.getClusters(); - expect(result).toStrictEqual([ + expect(result).toStrictEqual([ { - assumeRole: undefined, name: 'cluster1', - externalId: undefined, url: 'http://localhost:8080', authProvider: 'aws', - authMetadata: { serviceAccountToken: 'token' }, + authMetadata: { + serviceAccountToken: 'token', + }, skipTLSVerify: false, skipMetricsLookup: false, caData: undefined, caFile: undefined, }, { - assumeRole: 'SomeRole', name: 'cluster2', - externalId: undefined, url: 'http://localhost:8081', authProvider: 'aws', + authMetadata: { + assumeRole: 'SomeRole', + }, skipTLSVerify: true, skipMetricsLookup: false, caData: undefined, caFile: undefined, }, { - assumeRole: 'SomeRole', name: 'cluster2', - externalId: 'SomeExternalId', url: 'http://localhost:8081', authProvider: 'aws', + authMetadata: { + assumeRole: 'SomeRole', + externalId: 'SomeExternalId', + }, skipTLSVerify: true, skipMetricsLookup: false, caData: undefined, diff --git a/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.ts b/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.ts index e83bd763c4..161988d35b 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.ts @@ -73,7 +73,14 @@ export class ConfigClusterLocator implements KubernetesClustersSupplier { const assumeRole = c.getOptionalString('assumeRole'); const externalId = c.getOptionalString('externalId'); - return { assumeRole, externalId, ...clusterDetails }; + return { + authMetadata: { + ...(assumeRole && { assumeRole }), + ...(externalId && { externalId }), + ...clusterDetails.authMetadata, + }, + ...clusterDetails, + }; } case 'azure': { return clusterDetails; diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.test.ts b/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.test.ts index 30bb240a9f..8ba77491ab 100644 --- a/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.test.ts +++ b/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.test.ts @@ -70,10 +70,10 @@ describe('AwsIamKubernetesAuthTranslator tests', () => { const authTranslator = new AwsIamKubernetesAuthTranslator({ config }); const authPromise = authTranslator.decorateClusterDetailsWithAuth({ - assumeRole: 'SomeRole', name: 'test-cluster', url: '', authProvider: 'aws', + authMetadata: { assumeRole: 'SomeRole' }, }); expect((await authPromise).authMetadata!.serviceAccountToken).toEqual( 'k8s-aws-v1.aHR0cHM6Ly9odHRwczovL2V4YW1wbGUuY29tL2FzZGY_', @@ -96,11 +96,13 @@ describe('AwsIamKubernetesAuthTranslator tests', () => { const authTranslator = new AwsIamKubernetesAuthTranslator({ config }); const authPromise = authTranslator.decorateClusterDetailsWithAuth({ - assumeRole: 'SomeRole', - externalId: 'external-id', name: 'test-cluster', url: '', authProvider: 'aws', + authMetadata: { + assumeRole: 'SomeRole', + externalId: 'external-id', + }, }); expect((await authPromise).authMetadata!.serviceAccountToken).toEqual( 'k8s-aws-v1.aHR0cHM6Ly9odHRwczovL2V4YW1wbGUuY29tL2FzZGY_', diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.ts b/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.ts index c30cfd5bfd..1a1fb02fe3 100644 --- a/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.ts +++ b/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { AWSClusterDetails } from '../types/types'; +import { ClusterDetails } from '../types/types'; import { KubernetesAuthTranslator } from './types'; import { fromTemporaryCredentials } from '@aws-sdk/credential-providers'; import { SignatureV4 } from '@aws-sdk/signature-v4'; @@ -110,9 +110,9 @@ export class AwsIamKubernetesAuthTranslator } async decorateClusterDetailsWithAuth( - clusterDetails: AWSClusterDetails, - ): Promise { - const clusterDetailsWithAuthToken: AWSClusterDetails = Object.assign( + clusterDetails: ClusterDetails, + ): Promise { + const clusterDetailsWithAuthToken: ClusterDetails = Object.assign( {}, clusterDetails, ); @@ -120,8 +120,8 @@ export class AwsIamKubernetesAuthTranslator clusterDetailsWithAuthToken.authMetadata = { serviceAccountToken: await this.getBearerToken( clusterDetails.name, - clusterDetails.assumeRole, - clusterDetails.externalId, + clusterDetails.authMetadata?.assumeRole, + clusterDetails.authMetadata?.externalId, ), ...clusterDetailsWithAuthToken.authMetadata, }; diff --git a/plugins/kubernetes-backend/src/types/types.ts b/plugins/kubernetes-backend/src/types/types.ts index 6711462916..6d53ac350b 100644 --- a/plugins/kubernetes-backend/src/types/types.ts +++ b/plugins/kubernetes-backend/src/types/types.ts @@ -33,7 +33,7 @@ import { Config } from '@backstage/config'; */ export interface ObjectFetchParams { serviceId: string; - clusterDetails: AWSClusterDetails | GKEClusterDetails | ClusterDetails; + clusterDetails: GKEClusterDetails | ClusterDetails; objectTypesToFetch: Set; labelSelector: string; customResources: CustomResource[]; @@ -220,15 +220,6 @@ export interface GKEClusterDetails extends ClusterDetails {} */ export interface AzureClusterDetails extends ClusterDetails {} -/** - * - * @public - */ -export interface AWSClusterDetails extends ClusterDetails { - assumeRole?: string; - externalId?: string; -} - /** * * @public From 8dd179ac2f57b9b4743af58b38c35b477c1df071 Mon Sep 17 00:00:00 2001 From: Jamie Klassen Date: Tue, 5 Sep 2023 16:14:10 -0400 Subject: [PATCH 05/20] refactor: remove redundant ClusterDetails subtypes Signed-off-by: Jamie Klassen --- plugins/kubernetes-backend/api-report.md | 20 +++++++------------ .../src/cluster-locator/GkeClusterLocator.ts | 8 ++------ .../AzureIdentityKubernetesAuthTranslator.ts | 8 ++++---- .../GoogleKubernetesAuthTranslator.ts | 8 ++++---- .../GoogleServiceAccountAuthProvider.ts | 8 ++++---- plugins/kubernetes-backend/src/types/types.ts | 14 +------------ 6 files changed, 22 insertions(+), 44 deletions(-) diff --git a/plugins/kubernetes-backend/api-report.md b/plugins/kubernetes-backend/api-report.md index ebaae3dfa8..a9037f2c31 100644 --- a/plugins/kubernetes-backend/api-report.md +++ b/plugins/kubernetes-backend/api-report.md @@ -41,9 +41,6 @@ export class AwsIamKubernetesAuthTranslator ): Promise; } -// @public (undocumented) -export interface AzureClusterDetails extends ClusterDetails {} - // @public (undocumented) export class AzureIdentityKubernetesAuthTranslator implements KubernetesAuthTranslator @@ -51,8 +48,8 @@ export class AzureIdentityKubernetesAuthTranslator constructor(logger: Logger, tokenCredential?: TokenCredential); // (undocumented) decorateClusterDetailsWithAuth( - clusterDetails: AzureClusterDetails, - ): Promise; + clusterDetails: ClusterDetails, + ): Promise; } // @public (undocumented) @@ -123,18 +120,15 @@ export interface FetchResponseWrapper { responses: FetchResponse[]; } -// @public (undocumented) -export interface GKEClusterDetails extends ClusterDetails {} - // @public (undocumented) export class GoogleKubernetesAuthTranslator implements KubernetesAuthTranslator { // (undocumented) decorateClusterDetailsWithAuth( - clusterDetails: GKEClusterDetails, + clusterDetails: ClusterDetails, authConfig: KubernetesRequestAuth, - ): Promise; + ): Promise; } // @public (undocumented) @@ -143,8 +137,8 @@ export class GoogleServiceAccountAuthTranslator { // (undocumented) decorateClusterDetailsWithAuth( - clusterDetails: GKEClusterDetails, - ): Promise; + clusterDetails: ClusterDetails, + ): Promise; } // @public @@ -398,7 +392,7 @@ export class NoopKubernetesAuthTranslator implements KubernetesAuthTranslator { // @public (undocumented) export interface ObjectFetchParams { // (undocumented) - clusterDetails: GKEClusterDetails | ClusterDetails; + clusterDetails: ClusterDetails; // (undocumented) customResources: CustomResource[]; // (undocumented) diff --git a/plugins/kubernetes-backend/src/cluster-locator/GkeClusterLocator.ts b/plugins/kubernetes-backend/src/cluster-locator/GkeClusterLocator.ts index 7a81b98032..40a003607f 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/GkeClusterLocator.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/GkeClusterLocator.ts @@ -19,11 +19,7 @@ import { ForwardedError } from '@backstage/errors'; import * as container from '@google-cloud/container'; import { Duration } from 'luxon'; import { runPeriodically } from '../service/runPeriodically'; -import { - ClusterDetails, - GKEClusterDetails, - KubernetesClustersSupplier, -} from '../types/types'; +import { ClusterDetails, KubernetesClustersSupplier } from '../types/types'; interface MatchResourceLabelEntry { key: string; @@ -43,7 +39,7 @@ export class GkeClusterLocator implements KubernetesClustersSupplier { constructor( private readonly options: GkeClusterLocatorOptions, private readonly client: container.v1.ClusterManagerClient, - private clusterDetails: GKEClusterDetails[] | undefined = undefined, + private clusterDetails: ClusterDetails[] | undefined = undefined, private hasClusterDetails: boolean = false, ) {} diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/AzureIdentityKubernetesAuthTranslator.ts b/plugins/kubernetes-backend/src/kubernetes-auth-translator/AzureIdentityKubernetesAuthTranslator.ts index 784e3c7311..3e0622a9d2 100644 --- a/plugins/kubernetes-backend/src/kubernetes-auth-translator/AzureIdentityKubernetesAuthTranslator.ts +++ b/plugins/kubernetes-backend/src/kubernetes-auth-translator/AzureIdentityKubernetesAuthTranslator.ts @@ -16,7 +16,7 @@ import { Logger } from 'winston'; import { KubernetesAuthTranslator } from './types'; -import { AzureClusterDetails } from '../types/types'; +import { ClusterDetails } from '../types/types'; import { AccessToken, DefaultAzureCredential, @@ -41,9 +41,9 @@ export class AzureIdentityKubernetesAuthTranslator ) {} async decorateClusterDetailsWithAuth( - clusterDetails: AzureClusterDetails, - ): Promise { - const clusterDetailsWithAuthToken: AzureClusterDetails = Object.assign( + clusterDetails: ClusterDetails, + ): Promise { + const clusterDetailsWithAuthToken: ClusterDetails = Object.assign( {}, clusterDetails, ); diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/GoogleKubernetesAuthTranslator.ts b/plugins/kubernetes-backend/src/kubernetes-auth-translator/GoogleKubernetesAuthTranslator.ts index 96b2daebad..d0bef9c21b 100644 --- a/plugins/kubernetes-backend/src/kubernetes-auth-translator/GoogleKubernetesAuthTranslator.ts +++ b/plugins/kubernetes-backend/src/kubernetes-auth-translator/GoogleKubernetesAuthTranslator.ts @@ -15,7 +15,7 @@ */ import { KubernetesAuthTranslator } from './types'; -import { GKEClusterDetails } from '../types/types'; +import { ClusterDetails } from '../types/types'; import { KubernetesRequestAuth } from '@backstage/plugin-kubernetes-common'; /** @@ -26,10 +26,10 @@ export class GoogleKubernetesAuthTranslator implements KubernetesAuthTranslator { async decorateClusterDetailsWithAuth( - clusterDetails: GKEClusterDetails, + clusterDetails: ClusterDetails, authConfig: KubernetesRequestAuth, - ): Promise { - const clusterDetailsWithAuthToken: GKEClusterDetails = Object.assign( + ): Promise { + const clusterDetailsWithAuthToken: ClusterDetails = Object.assign( {}, clusterDetails, ); diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/GoogleServiceAccountAuthProvider.ts b/plugins/kubernetes-backend/src/kubernetes-auth-translator/GoogleServiceAccountAuthProvider.ts index 81b50c478b..8aa60e0012 100644 --- a/plugins/kubernetes-backend/src/kubernetes-auth-translator/GoogleServiceAccountAuthProvider.ts +++ b/plugins/kubernetes-backend/src/kubernetes-auth-translator/GoogleServiceAccountAuthProvider.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import { KubernetesAuthTranslator } from './types'; -import { GKEClusterDetails } from '../types/types'; +import { ClusterDetails } from '../types/types'; import * as container from '@google-cloud/container'; /** @@ -25,9 +25,9 @@ export class GoogleServiceAccountAuthTranslator implements KubernetesAuthTranslator { async decorateClusterDetailsWithAuth( - clusterDetails: GKEClusterDetails, - ): Promise { - const clusterDetailsWithAuthToken: GKEClusterDetails = Object.assign( + clusterDetails: ClusterDetails, + ): Promise { + const clusterDetailsWithAuthToken: ClusterDetails = Object.assign( {}, clusterDetails, ); diff --git a/plugins/kubernetes-backend/src/types/types.ts b/plugins/kubernetes-backend/src/types/types.ts index 6d53ac350b..0096c166d8 100644 --- a/plugins/kubernetes-backend/src/types/types.ts +++ b/plugins/kubernetes-backend/src/types/types.ts @@ -33,7 +33,7 @@ import { Config } from '@backstage/config'; */ export interface ObjectFetchParams { serviceId: string; - clusterDetails: GKEClusterDetails | ClusterDetails; + clusterDetails: ClusterDetails; objectTypesToFetch: Set; labelSelector: string; customResources: CustomResource[]; @@ -208,18 +208,6 @@ export interface ClusterDetails { authMetadata?: Record; } -/** - * - * @public - */ -export interface GKEClusterDetails extends ClusterDetails {} - -/** - * - * @public - */ -export interface AzureClusterDetails extends ClusterDetails {} - /** * * @public From 1449a035ce439f40e9f85714c92cff0cfe4e22da Mon Sep 17 00:00:00 2001 From: Jamie Klassen Date: Tue, 5 Sep 2023 17:05:33 -0400 Subject: [PATCH 06/20] migrate oidcTokenProvider to authMetadata Signed-off-by: Jamie Klassen --- plugins/kubernetes-backend/api-report.md | 2 -- .../cluster-locator/CatalogClusterLocator.test.ts | 4 ++-- .../src/cluster-locator/CatalogClusterLocator.ts | 10 ++++++---- .../src/cluster-locator/ConfigClusterLocator.ts | 8 +++++++- .../OidcKubernetesAuthTranslator.test.ts | 4 ++-- .../OidcKubernetesAuthTranslator.ts | 2 +- .../src/service/KubernetesBuilder.ts | 15 +++++++++------ plugins/kubernetes-backend/src/types/types.ts | 6 ++---- 8 files changed, 29 insertions(+), 22 deletions(-) diff --git a/plugins/kubernetes-backend/api-report.md b/plugins/kubernetes-backend/api-report.md index a9037f2c31..37155edc1b 100644 --- a/plugins/kubernetes-backend/api-report.md +++ b/plugins/kubernetes-backend/api-report.md @@ -54,7 +54,6 @@ export class AzureIdentityKubernetesAuthTranslator // @public (undocumented) export interface ClusterDetails { - // (undocumented) authMetadata?: Record; // (undocumented) authProvider: string; @@ -67,7 +66,6 @@ export interface ClusterDetails { dashboardParameters?: JsonObject; dashboardUrl?: string; name: string; - oidcTokenProvider?: string | undefined; skipMetricsLookup?: boolean; // (undocumented) skipTLSVerify?: boolean; diff --git a/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.test.ts b/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.test.ts index 86776f4d02..8a4120ca56 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.test.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.test.ts @@ -92,7 +92,7 @@ describe('CatalogClusterLocator', () => { url: 'https://apiserver.com', caData: 'caData', authProvider: 'oidc', - oidcTokenProvider: 'google', + authMetadata: { oidcTokenProvider: 'google' }, skipMetricsLookup: true, skipTLSVerify: true, dashboardUrl: 'my-url', @@ -114,8 +114,8 @@ describe('CatalogClusterLocator', () => { authMetadata: { assumeRole: 'my-role', externalId: 'my-id', + oidcTokenProvider: 'google', }, - oidcTokenProvider: 'google', skipMetricsLookup: false, skipTLSVerify: false, dashboardUrl: 'my-url', diff --git a/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.ts b/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.ts index 9d81785a28..5fcc3d43a8 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.ts @@ -57,6 +57,10 @@ export class CatalogClusterLocator implements KubernetesClustersSupplier { filter: [filter], }); return clusters.items.map(entity => { + const oidcTokenProvider = + entity.metadata.annotations?.[ + ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER + ]; const clusterDetails: ClusterDetails = { name: entity.metadata.name, url: entity.metadata.annotations![ANNOTATION_KUBERNETES_API_SERVER]!, @@ -64,10 +68,7 @@ export class CatalogClusterLocator implements KubernetesClustersSupplier { entity.metadata.annotations![ANNOTATION_KUBERNETES_API_SERVER_CA]!, authProvider: entity.metadata.annotations![ANNOTATION_KUBERNETES_AUTH_PROVIDER]!, - oidcTokenProvider: - entity.metadata.annotations![ - ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER - ]!, + ...(oidcTokenProvider && { authMetadata: { oidcTokenProvider } }), skipMetricsLookup: entity.metadata.annotations![ ANNOTATION_KUBERNETES_SKIP_METRICS_LOOKUP @@ -94,6 +95,7 @@ export class CatalogClusterLocator implements KubernetesClustersSupplier { entity.metadata.annotations![ ANNOTATION_KUBERNETES_AWS_EXTERNAL_ID ]!, + ...clusterDetails.authMetadata, }, }; } diff --git a/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.ts b/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.ts index 161988d35b..4dbc083f6a 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.ts @@ -88,7 +88,13 @@ export class ConfigClusterLocator implements KubernetesClustersSupplier { case 'oidc': { const oidcTokenProvider = c.getString('oidcTokenProvider'); - return { oidcTokenProvider, ...clusterDetails }; + return { + authMetadata: { + oidcTokenProvider, + ...clusterDetails.authMetadata, + }, + ...clusterDetails, + }; } case 'serviceAccount': { return clusterDetails; diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/OidcKubernetesAuthTranslator.test.ts b/plugins/kubernetes-backend/src/kubernetes-auth-translator/OidcKubernetesAuthTranslator.test.ts index fe604008f1..f8184296e5 100644 --- a/plugins/kubernetes-backend/src/kubernetes-auth-translator/OidcKubernetesAuthTranslator.test.ts +++ b/plugins/kubernetes-backend/src/kubernetes-auth-translator/OidcKubernetesAuthTranslator.test.ts @@ -28,7 +28,7 @@ describe('OidcKubernetesAuthTranslator tests', () => { it('returns cluster details with auth token', async () => { const details = await at.decorateClusterDetailsWithAuth( { - oidcTokenProvider: 'okta', + authMetadata: { oidcTokenProvider: 'okta' }, ...baseClusterDetails, }, { @@ -50,7 +50,7 @@ describe('OidcKubernetesAuthTranslator tests', () => { it('returns error when token is not included in request body', async () => { await expect( at.decorateClusterDetailsWithAuth( - { oidcTokenProvider: 'okta', ...baseClusterDetails }, + { authMetadata: { oidcTokenProvider: 'okta' }, ...baseClusterDetails }, {}, ), ).rejects.toThrow('Auth token not found under oidc.okta in request body'); diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/OidcKubernetesAuthTranslator.ts b/plugins/kubernetes-backend/src/kubernetes-auth-translator/OidcKubernetesAuthTranslator.ts index 5c0c0de181..c72e2fff71 100644 --- a/plugins/kubernetes-backend/src/kubernetes-auth-translator/OidcKubernetesAuthTranslator.ts +++ b/plugins/kubernetes-backend/src/kubernetes-auth-translator/OidcKubernetesAuthTranslator.ts @@ -32,7 +32,7 @@ export class OidcKubernetesAuthTranslator implements KubernetesAuthTranslator { clusterDetails, ); - const { oidcTokenProvider } = clusterDetails; + const oidcTokenProvider = clusterDetails.authMetadata?.oidcTokenProvider; if (!oidcTokenProvider || oidcTokenProvider === '') { throw new Error( diff --git a/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts b/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts index 8f9d0769ed..905ef521a3 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts @@ -339,12 +339,15 @@ export class KubernetesBuilder { router.get('/clusters', async (_, res) => { const clusterDetails = await this.fetchClusterDetails(clusterSupplier); res.json({ - items: clusterDetails.map(cd => ({ - name: cd.name, - dashboardUrl: cd.dashboardUrl, - authProvider: cd.authProvider, - oidcTokenProvider: cd.oidcTokenProvider, - })), + items: clusterDetails.map(cd => { + const oidcTokenProvider = cd.authMetadata?.oidcTokenProvider; + return { + name: cd.name, + dashboardUrl: cd.dashboardUrl, + authProvider: cd.authProvider, + ...(oidcTokenProvider && { oidcTokenProvider }), + }; + }), }); }); diff --git a/plugins/kubernetes-backend/src/types/types.ts b/plugins/kubernetes-backend/src/types/types.ts index 0096c166d8..890ebd73a3 100644 --- a/plugins/kubernetes-backend/src/types/types.ts +++ b/plugins/kubernetes-backend/src/types/types.ts @@ -156,9 +156,9 @@ export interface ClusterDetails { url: string; authProvider: string; /** - * oidc provider used to get id tokens to authenticate against kubernetes + * Provider-specific authentication configuration */ - oidcTokenProvider?: string | undefined; + authMetadata?: Record; skipTLSVerify?: boolean; /** * Whether to skip the lookup to the metrics server to retrieve pod resource usage. @@ -204,8 +204,6 @@ export interface ClusterDetails { * Kubernetes resources. */ customResources?: CustomResourceMatcher[]; - - authMetadata?: Record; } /** From 7a914fcff1868c31bfc71deadbe74969f014564c Mon Sep 17 00:00:00 2001 From: Jamie Klassen Date: Tue, 5 Sep 2023 17:37:51 -0400 Subject: [PATCH 07/20] use existing annotations as metadata keys Signed-off-by: Jamie Klassen --- .../CatalogClusterLocator.test.ts | 43 +++++++++++++------ .../cluster-locator/CatalogClusterLocator.ts | 26 +---------- .../ConfigClusterLocator.test.ts | 10 +++-- .../cluster-locator/ConfigClusterLocator.ts | 15 +++++-- .../AwsIamKubernetesAuthTranslator.test.ts | 12 ++++-- .../AwsIamKubernetesAuthTranslator.ts | 12 ++++-- .../OidcKubernetesAuthTranslator.test.ts | 8 +++- .../OidcKubernetesAuthTranslator.ts | 9 ++-- 8 files changed, 79 insertions(+), 56 deletions(-) diff --git a/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.test.ts b/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.test.ts index 8a4120ca56..2f6d3297e1 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.test.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.test.ts @@ -15,6 +15,11 @@ */ import '@backstage/backend-common'; +import { + ANNOTATION_KUBERNETES_AWS_ASSUME_ROLE, + ANNOTATION_KUBERNETES_AWS_EXTERNAL_ID, + ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER, +} from '@backstage/plugin-kubernetes-common'; import { CatalogClusterLocator } from './CatalogClusterLocator'; import { CatalogApi } from '@backstage/catalog-client'; import { ClusterDetails } from '../types/types'; @@ -24,14 +29,14 @@ const mockCatalogApi = { getEntities: async () => ({ items: [ { - apiVersion: 'version', - kind: 'User', + apiVersion: 'backstage.io/v1alpha1', + kind: 'Resource', metadata: { annotations: { 'kubernetes.io/api-server': 'https://apiserver.com', 'kubernetes.io/api-server-certificate-authority': 'caData', 'kubernetes.io/auth-provider': 'oidc', - 'kubernetes.io/oidc-token-provider': 'google', + [ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER]: 'google', 'kubernetes.io/skip-metrics-lookup': 'true', 'kubernetes.io/skip-tls-verify': 'true', 'kubernetes.io/dashboard-url': 'my-url', @@ -42,16 +47,16 @@ const mockCatalogApi = { }, }, { - apiVersion: 'version', - kind: 'User', + apiVersion: 'backstage.io/v1alpha1', + kind: 'Resource', metadata: { annotations: { 'kubernetes.io/api-server': 'https://apiserver.com', 'kubernetes.io/api-server-certificate-authority': 'caData', 'kubernetes.io/auth-provider': 'aws', - 'kubernetes.io/aws-assume-role': 'my-role', - 'kubernetes.io/aws-external-id': 'my-id', - 'kubernetes.io/oidc-token-provider': 'google', + [ANNOTATION_KUBERNETES_AWS_ASSUME_ROLE]: 'my-role', + [ANNOTATION_KUBERNETES_AWS_EXTERNAL_ID]: 'my-id', + [ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER]: 'google', 'kubernetes.io/dashboard-url': 'my-url', 'kubernetes.io/dashboard-app': 'my-app', }, @@ -92,7 +97,16 @@ describe('CatalogClusterLocator', () => { url: 'https://apiserver.com', caData: 'caData', authProvider: 'oidc', - authMetadata: { oidcTokenProvider: 'google' }, + authMetadata: { + 'kubernetes.io/api-server': 'https://apiserver.com', + 'kubernetes.io/api-server-certificate-authority': 'caData', + 'kubernetes.io/auth-provider': 'oidc', + [ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER]: 'google', + 'kubernetes.io/skip-metrics-lookup': 'true', + 'kubernetes.io/skip-tls-verify': 'true', + 'kubernetes.io/dashboard-url': 'my-url', + 'kubernetes.io/dashboard-app': 'my-app', + }, skipMetricsLookup: true, skipTLSVerify: true, dashboardUrl: 'my-url', @@ -112,9 +126,14 @@ describe('CatalogClusterLocator', () => { caData: 'caData', authProvider: 'aws', authMetadata: { - assumeRole: 'my-role', - externalId: 'my-id', - oidcTokenProvider: 'google', + 'kubernetes.io/api-server': 'https://apiserver.com', + 'kubernetes.io/api-server-certificate-authority': 'caData', + 'kubernetes.io/auth-provider': 'aws', + [ANNOTATION_KUBERNETES_AWS_ASSUME_ROLE]: 'my-role', + [ANNOTATION_KUBERNETES_AWS_EXTERNAL_ID]: 'my-id', + [ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER]: 'google', + 'kubernetes.io/dashboard-url': 'my-url', + 'kubernetes.io/dashboard-app': 'my-app', }, skipMetricsLookup: false, skipTLSVerify: false, diff --git a/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.ts b/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.ts index 5fcc3d43a8..a0088d4b79 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.ts @@ -20,13 +20,10 @@ import { ANNOTATION_KUBERNETES_API_SERVER, ANNOTATION_KUBERNETES_API_SERVER_CA, ANNOTATION_KUBERNETES_AUTH_PROVIDER, - ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER, ANNOTATION_KUBERNETES_SKIP_METRICS_LOOKUP, ANNOTATION_KUBERNETES_SKIP_TLS_VERIFY, ANNOTATION_KUBERNETES_DASHBOARD_URL, ANNOTATION_KUBERNETES_DASHBOARD_APP, - ANNOTATION_KUBERNETES_AWS_EXTERNAL_ID, - ANNOTATION_KUBERNETES_AWS_ASSUME_ROLE, } from '@backstage/plugin-kubernetes-common'; export class CatalogClusterLocator implements KubernetesClustersSupplier { @@ -57,10 +54,6 @@ export class CatalogClusterLocator implements KubernetesClustersSupplier { filter: [filter], }); return clusters.items.map(entity => { - const oidcTokenProvider = - entity.metadata.annotations?.[ - ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER - ]; const clusterDetails: ClusterDetails = { name: entity.metadata.name, url: entity.metadata.annotations![ANNOTATION_KUBERNETES_API_SERVER]!, @@ -68,7 +61,7 @@ export class CatalogClusterLocator implements KubernetesClustersSupplier { entity.metadata.annotations![ANNOTATION_KUBERNETES_API_SERVER_CA]!, authProvider: entity.metadata.annotations![ANNOTATION_KUBERNETES_AUTH_PROVIDER]!, - ...(oidcTokenProvider && { authMetadata: { oidcTokenProvider } }), + ...{ authMetadata: entity.metadata.annotations }, skipMetricsLookup: entity.metadata.annotations![ ANNOTATION_KUBERNETES_SKIP_METRICS_LOOKUP @@ -83,23 +76,6 @@ export class CatalogClusterLocator implements KubernetesClustersSupplier { entity.metadata.annotations![ANNOTATION_KUBERNETES_DASHBOARD_APP]!, }; - if (clusterDetails.authProvider === 'aws') { - return { - ...clusterDetails, - authMetadata: { - assumeRole: - entity.metadata.annotations![ - ANNOTATION_KUBERNETES_AWS_ASSUME_ROLE - ]!, - externalId: - entity.metadata.annotations![ - ANNOTATION_KUBERNETES_AWS_EXTERNAL_ID - ]!, - ...clusterDetails.authMetadata, - }, - }; - } - return clusterDetails; }); } diff --git a/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.test.ts b/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.test.ts index 037a226476..0c9ad7aad4 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.test.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.test.ts @@ -16,6 +16,10 @@ import '@backstage/backend-common'; import { ConfigReader, Config } from '@backstage/config'; +import { + ANNOTATION_KUBERNETES_AWS_ASSUME_ROLE, + ANNOTATION_KUBERNETES_AWS_EXTERNAL_ID, +} from '@backstage/plugin-kubernetes-common'; import { ConfigClusterLocator } from './ConfigClusterLocator'; import { ClusterDetails } from '../types/types'; @@ -160,7 +164,7 @@ describe('ConfigClusterLocator', () => { url: 'http://localhost:8081', authProvider: 'aws', authMetadata: { - assumeRole: 'SomeRole', + [ANNOTATION_KUBERNETES_AWS_ASSUME_ROLE]: 'SomeRole', }, skipTLSVerify: true, skipMetricsLookup: false, @@ -172,8 +176,8 @@ describe('ConfigClusterLocator', () => { url: 'http://localhost:8081', authProvider: 'aws', authMetadata: { - assumeRole: 'SomeRole', - externalId: 'SomeExternalId', + [ANNOTATION_KUBERNETES_AWS_ASSUME_ROLE]: 'SomeRole', + [ANNOTATION_KUBERNETES_AWS_EXTERNAL_ID]: 'SomeExternalId', }, skipTLSVerify: true, skipMetricsLookup: false, diff --git a/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.ts b/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.ts index 4dbc083f6a..503aadeb9d 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.ts @@ -15,6 +15,11 @@ */ import { Config } from '@backstage/config'; +import { + ANNOTATION_KUBERNETES_AWS_ASSUME_ROLE, + ANNOTATION_KUBERNETES_AWS_EXTERNAL_ID, + ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER, +} from '@backstage/plugin-kubernetes-common'; import { ClusterDetails, KubernetesClustersSupplier } from '../types/types'; export class ConfigClusterLocator implements KubernetesClustersSupplier { @@ -75,8 +80,12 @@ export class ConfigClusterLocator implements KubernetesClustersSupplier { return { authMetadata: { - ...(assumeRole && { assumeRole }), - ...(externalId && { externalId }), + ...(assumeRole && { + [ANNOTATION_KUBERNETES_AWS_ASSUME_ROLE]: assumeRole, + }), + ...(externalId && { + [ANNOTATION_KUBERNETES_AWS_EXTERNAL_ID]: externalId, + }), ...clusterDetails.authMetadata, }, ...clusterDetails, @@ -90,7 +99,7 @@ export class ConfigClusterLocator implements KubernetesClustersSupplier { return { authMetadata: { - oidcTokenProvider, + [ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER]: oidcTokenProvider, ...clusterDetails.authMetadata, }, ...clusterDetails, diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.test.ts b/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.test.ts index 8ba77491ab..b024283c20 100644 --- a/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.test.ts +++ b/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.test.ts @@ -13,8 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { AwsIamKubernetesAuthTranslator } from './AwsIamKubernetesAuthTranslator'; import { ConfigReader } from '@backstage/config'; +import { + ANNOTATION_KUBERNETES_AWS_ASSUME_ROLE, + ANNOTATION_KUBERNETES_AWS_EXTERNAL_ID, +} from '@backstage/plugin-kubernetes-common'; +import { AwsIamKubernetesAuthTranslator } from './AwsIamKubernetesAuthTranslator'; let presign = jest.fn(async () => ({ hostname: 'https://example.com', @@ -73,7 +77,7 @@ describe('AwsIamKubernetesAuthTranslator tests', () => { name: 'test-cluster', url: '', authProvider: 'aws', - authMetadata: { assumeRole: 'SomeRole' }, + authMetadata: { [ANNOTATION_KUBERNETES_AWS_ASSUME_ROLE]: 'SomeRole' }, }); expect((await authPromise).authMetadata!.serviceAccountToken).toEqual( 'k8s-aws-v1.aHR0cHM6Ly9odHRwczovL2V4YW1wbGUuY29tL2FzZGY_', @@ -100,8 +104,8 @@ describe('AwsIamKubernetesAuthTranslator tests', () => { url: '', authProvider: 'aws', authMetadata: { - assumeRole: 'SomeRole', - externalId: 'external-id', + [ANNOTATION_KUBERNETES_AWS_ASSUME_ROLE]: 'SomeRole', + [ANNOTATION_KUBERNETES_AWS_EXTERNAL_ID]: 'external-id', }, }); expect((await authPromise).authMetadata!.serviceAccountToken).toEqual( diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.ts b/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.ts index 1a1fb02fe3..d76ae985de 100644 --- a/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.ts +++ b/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.ts @@ -13,8 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { ClusterDetails } from '../types/types'; -import { KubernetesAuthTranslator } from './types'; import { fromTemporaryCredentials } from '@aws-sdk/credential-providers'; import { SignatureV4 } from '@aws-sdk/signature-v4'; import { Sha256 } from '@aws-crypto/sha256-js'; @@ -23,6 +21,12 @@ import { DefaultAwsCredentialsManager, } from '@backstage/integration-aws-node'; import { Config } from '@backstage/config'; +import { + ANNOTATION_KUBERNETES_AWS_ASSUME_ROLE, + ANNOTATION_KUBERNETES_AWS_EXTERNAL_ID, +} from '@backstage/plugin-kubernetes-common'; +import { ClusterDetails } from '../types/types'; +import { KubernetesAuthTranslator } from './types'; /** * @@ -120,8 +124,8 @@ export class AwsIamKubernetesAuthTranslator clusterDetailsWithAuthToken.authMetadata = { serviceAccountToken: await this.getBearerToken( clusterDetails.name, - clusterDetails.authMetadata?.assumeRole, - clusterDetails.authMetadata?.externalId, + clusterDetails.authMetadata?.[ANNOTATION_KUBERNETES_AWS_ASSUME_ROLE], + clusterDetails.authMetadata?.[ANNOTATION_KUBERNETES_AWS_EXTERNAL_ID], ), ...clusterDetailsWithAuthToken.authMetadata, }; diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/OidcKubernetesAuthTranslator.test.ts b/plugins/kubernetes-backend/src/kubernetes-auth-translator/OidcKubernetesAuthTranslator.test.ts index f8184296e5..2641922b3a 100644 --- a/plugins/kubernetes-backend/src/kubernetes-auth-translator/OidcKubernetesAuthTranslator.test.ts +++ b/plugins/kubernetes-backend/src/kubernetes-auth-translator/OidcKubernetesAuthTranslator.test.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER } from '@backstage/plugin-kubernetes-common'; import { OidcKubernetesAuthTranslator } from './OidcKubernetesAuthTranslator'; import { ClusterDetails } from '../types/types'; @@ -28,7 +29,7 @@ describe('OidcKubernetesAuthTranslator tests', () => { it('returns cluster details with auth token', async () => { const details = await at.decorateClusterDetailsWithAuth( { - authMetadata: { oidcTokenProvider: 'okta' }, + authMetadata: { [ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER]: 'okta' }, ...baseClusterDetails, }, { @@ -50,7 +51,10 @@ describe('OidcKubernetesAuthTranslator tests', () => { it('returns error when token is not included in request body', async () => { await expect( at.decorateClusterDetailsWithAuth( - { authMetadata: { oidcTokenProvider: 'okta' }, ...baseClusterDetails }, + { + authMetadata: { [ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER]: 'okta' }, + ...baseClusterDetails, + }, {}, ), ).rejects.toThrow('Auth token not found under oidc.okta in request body'); diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/OidcKubernetesAuthTranslator.ts b/plugins/kubernetes-backend/src/kubernetes-auth-translator/OidcKubernetesAuthTranslator.ts index c72e2fff71..ef13bfb146 100644 --- a/plugins/kubernetes-backend/src/kubernetes-auth-translator/OidcKubernetesAuthTranslator.ts +++ b/plugins/kubernetes-backend/src/kubernetes-auth-translator/OidcKubernetesAuthTranslator.ts @@ -13,10 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - +import { + ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER, + KubernetesRequestAuth, +} from '@backstage/plugin-kubernetes-common'; import { KubernetesAuthTranslator } from './types'; import { ClusterDetails } from '../types/types'; -import { KubernetesRequestAuth } from '@backstage/plugin-kubernetes-common'; /** * @@ -32,7 +34,8 @@ export class OidcKubernetesAuthTranslator implements KubernetesAuthTranslator { clusterDetails, ); - const oidcTokenProvider = clusterDetails.authMetadata?.oidcTokenProvider; + const oidcTokenProvider = + clusterDetails.authMetadata?.[ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER]; if (!oidcTokenProvider || oidcTokenProvider === '') { throw new Error( From 9327871142d2a918f8624369bfc4d942a00d6659 Mon Sep 17 00:00:00 2001 From: Jamie Klassen Date: Tue, 5 Sep 2023 18:03:01 -0400 Subject: [PATCH 08/20] parse auth metadata in a single pass validate the resulting metadata object instead of the config directly. Signed-off-by: Jamie Klassen --- .../ConfigClusterLocator.test.ts | 15 ++++ .../cluster-locator/ConfigClusterLocator.ts | 69 +++++++++++-------- 2 files changed, 56 insertions(+), 28 deletions(-) diff --git a/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.test.ts b/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.test.ts index 0c9ad7aad4..e1229aecf3 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.test.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.test.ts @@ -315,4 +315,19 @@ describe('ConfigClusterLocator', () => { }, ]); }); + + it('errors when authProvider is oidc but oidcTokenProvider is missing', async () => { + const cluster = { + name: 'oidc-cluster', + url: 'https://aks.test', + authProvider: 'oidc', + }; + expect(() => + ConfigClusterLocator.fromConfig( + new ConfigReader({ clusters: [cluster] }), + ), + ).toThrow( + `Cluster 'oidc-cluster' missing required config value for 'oidcTokenProvider'`, + ); + }); }); diff --git a/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.ts b/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.ts index 503aadeb9d..b69811af33 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.ts @@ -30,12 +30,9 @@ export class ConfigClusterLocator implements KubernetesClustersSupplier { } static fromConfig(config: Config): ConfigClusterLocator { - // TODO: Add validation that authProvider is required and serviceAccountToken - // is required if authProvider is serviceAccount return new ConfigClusterLocator( config.getConfigArray('clusters').map(c => { const authProvider = c.getString('authProvider'); - const serviceAccountToken = c.getOptionalString('serviceAccountToken'); const clusterDetails: ClusterDetails = { name: c.getString('name'), url: c.getString('url'), @@ -44,7 +41,7 @@ export class ConfigClusterLocator implements KubernetesClustersSupplier { caData: c.getOptionalString('caData'), caFile: c.getOptionalString('caFile'), authProvider: authProvider, - ...(serviceAccountToken && { authMetadata: { serviceAccountToken } }), + ...ConfigClusterLocator.parseAuthMetadata(c), }; const customResources = c.getOptionalConfigArray('customResources'); @@ -75,35 +72,22 @@ export class ConfigClusterLocator implements KubernetesClustersSupplier { return clusterDetails; } case 'aws': { - const assumeRole = c.getOptionalString('assumeRole'); - const externalId = c.getOptionalString('externalId'); - - return { - authMetadata: { - ...(assumeRole && { - [ANNOTATION_KUBERNETES_AWS_ASSUME_ROLE]: assumeRole, - }), - ...(externalId && { - [ANNOTATION_KUBERNETES_AWS_EXTERNAL_ID]: externalId, - }), - ...clusterDetails.authMetadata, - }, - ...clusterDetails, - }; + return clusterDetails; } case 'azure': { return clusterDetails; } case 'oidc': { - const oidcTokenProvider = c.getString('oidcTokenProvider'); - - return { - authMetadata: { - [ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER]: oidcTokenProvider, - ...clusterDetails.authMetadata, - }, - ...clusterDetails, - }; + if ( + !clusterDetails.authMetadata?.[ + ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER + ] + ) { + throw new Error( + `Cluster '${clusterDetails.name}' missing required config value for 'oidcTokenProvider'`, + ); + } + return clusterDetails; } case 'serviceAccount': { return clusterDetails; @@ -124,6 +108,35 @@ export class ConfigClusterLocator implements KubernetesClustersSupplier { ); } + private static parseAuthMetadata( + clusterConfig: Config, + ): { authMetadata: Record } | undefined { + const serviceAccountToken = clusterConfig.getOptionalString( + 'serviceAccountToken', + ); + const assumeRole = clusterConfig.getOptionalString('assumeRole'); + const externalId = clusterConfig.getOptionalString('externalId'); + const oidcTokenProvider = + clusterConfig.getOptionalString('oidcTokenProvider'); + + return serviceAccountToken || assumeRole || externalId + ? { + authMetadata: { + ...(serviceAccountToken && { serviceAccountToken }), + ...(assumeRole && { + [ANNOTATION_KUBERNETES_AWS_ASSUME_ROLE]: assumeRole, + }), + ...(externalId && { + [ANNOTATION_KUBERNETES_AWS_EXTERNAL_ID]: externalId, + }), + ...(oidcTokenProvider && { + [ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER]: oidcTokenProvider, + }), + }, + } + : undefined; + } + async getClusters(): Promise { return this.clusterDetails; } From d930afdb434c412251304023d4c9f70a0cd971a5 Mon Sep 17 00:00:00 2001 From: Jamie Klassen Date: Tue, 5 Sep 2023 18:12:38 -0400 Subject: [PATCH 09/20] decouple auth validation from config parsing Eventually this map can be injected the same way the authTranslatorMap is. Signed-off-by: Jamie Klassen --- .../cluster-locator/ConfigClusterLocator.ts | 67 +++++++++---------- 1 file changed, 30 insertions(+), 37 deletions(-) diff --git a/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.ts b/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.ts index b69811af33..acbb5f96fd 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.ts @@ -25,6 +25,29 @@ import { ClusterDetails, KubernetesClustersSupplier } from '../types/types'; export class ConfigClusterLocator implements KubernetesClustersSupplier { private readonly clusterDetails: ClusterDetails[]; + private static authValidators: Record< + string, + (clusterDetails: ClusterDetails) => void + > = { + google: _ => {}, + aws: _ => {}, + azure: _ => {}, + oidc: clusterDetails => { + if ( + !clusterDetails.authMetadata?.[ + ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER + ] + ) { + throw new Error( + `Cluster '${clusterDetails.name}' missing required config value for 'oidcTokenProvider'`, + ); + } + }, + serviceAccount: _ => {}, + googleServiceAccount: _ => {}, + aks: _ => {}, + }; + constructor(clusterDetails: ClusterDetails[]) { this.clusterDetails = clusterDetails; } @@ -67,43 +90,13 @@ export class ConfigClusterLocator implements KubernetesClustersSupplier { clusterDetails.dashboardParameters = c.get('dashboardParameters'); } - switch (authProvider) { - case 'google': { - return clusterDetails; - } - case 'aws': { - return clusterDetails; - } - case 'azure': { - return clusterDetails; - } - case 'oidc': { - if ( - !clusterDetails.authMetadata?.[ - ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER - ] - ) { - throw new Error( - `Cluster '${clusterDetails.name}' missing required config value for 'oidcTokenProvider'`, - ); - } - return clusterDetails; - } - case 'serviceAccount': { - return clusterDetails; - } - case 'googleServiceAccount': { - return clusterDetails; - } - case 'aks': { - return clusterDetails; - } - default: { - throw new Error( - `authProvider "${authProvider}" has no config associated with it`, - ); - } + if (authProvider in ConfigClusterLocator.authValidators) { + ConfigClusterLocator.authValidators[authProvider](clusterDetails); + return clusterDetails; } + throw new Error( + `authProvider "${authProvider}" has no config associated with it`, + ); }), ); } @@ -119,7 +112,7 @@ export class ConfigClusterLocator implements KubernetesClustersSupplier { const oidcTokenProvider = clusterConfig.getOptionalString('oidcTokenProvider'); - return serviceAccountToken || assumeRole || externalId + return serviceAccountToken || assumeRole || externalId || oidcTokenProvider ? { authMetadata: { ...(serviceAccountToken && { serviceAccountToken }), From 54c957c1bc96aaa1718efc7a0b40c18b64c64427 Mon Sep 17 00:00:00 2001 From: Jamie Klassen Date: Wed, 6 Sep 2023 11:22:30 -0400 Subject: [PATCH 10/20] rename translators to strategies to encompass their upcoming new responsibilities Signed-off-by: Jamie Klassen --- plugins/kubernetes-backend/api-report.md | 72 ++++++++---------- .../AksStrategy.test.ts} | 8 +- .../AksStrategy.ts} | 3 +- .../AwsIamStrategy.test.ts} | 20 ++--- .../AwsIamStrategy.ts} | 6 +- .../AzureIdentityStrategy.test.ts} | 40 +++++----- .../AzureIdentityStrategy.ts} | 6 +- .../DispatchStrategy.test.ts} | 30 ++++---- .../DispatchStrategy.ts} | 26 +++---- .../GoogleServiceAccountStrategy.ts} | 6 +- .../GoogleStrategy.ts} | 6 +- .../NoopStrategy.ts} | 4 +- .../OidcStrategy.test.ts} | 12 +-- .../OidcStrategy.ts} | 4 +- .../index.ts | 16 ++-- .../types.ts | 2 +- plugins/kubernetes-backend/src/index.ts | 2 +- .../src/service/KubernetesBuilder.ts | 74 +++++++++---------- .../service/KubernetesFanOutHandler.test.ts | 4 +- .../src/service/KubernetesFanOutHandler.ts | 12 +-- .../src/service/KubernetesProxy.test.ts | 43 +++++------ .../src/service/KubernetesProxy.ts | 12 ++- 22 files changed, 191 insertions(+), 217 deletions(-) rename plugins/kubernetes-backend/src/{kubernetes-auth-translator/AksKubernetesAuthTranslator.test.ts => auth/AksStrategy.test.ts} (76%) rename plugins/kubernetes-backend/src/{kubernetes-auth-translator/AksKubernetesAuthTranslator.ts => auth/AksStrategy.ts} (90%) rename plugins/kubernetes-backend/src/{kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.test.ts => auth/AwsIamStrategy.test.ts} (83%) rename plugins/kubernetes-backend/src/{kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.ts => auth/AwsIamStrategy.ts} (96%) rename plugins/kubernetes-backend/src/{kubernetes-auth-translator/AzureIdentityKubernetesAuthTranslator.test.ts => auth/AzureIdentityStrategy.test.ts} (70%) rename plugins/kubernetes-backend/src/{kubernetes-auth-translator/AzureIdentityKubernetesAuthTranslator.ts => auth/AzureIdentityStrategy.ts} (95%) rename plugins/kubernetes-backend/src/{kubernetes-auth-translator/DispatchingKubernetesAuthTranslator.test.ts => auth/DispatchStrategy.test.ts} (60%) rename plugins/kubernetes-backend/src/{kubernetes-auth-translator/DispatchingKubernetesAuthTranslator.ts => auth/DispatchStrategy.ts} (61%) rename plugins/kubernetes-backend/src/{kubernetes-auth-translator/GoogleServiceAccountAuthProvider.ts => auth/GoogleServiceAccountStrategy.ts} (91%) rename plugins/kubernetes-backend/src/{kubernetes-auth-translator/GoogleKubernetesAuthTranslator.ts => auth/GoogleStrategy.ts} (91%) rename plugins/kubernetes-backend/src/{kubernetes-auth-translator/NoopKubernetesAuthTranslator.ts => auth/NoopStrategy.ts} (86%) rename plugins/kubernetes-backend/src/{kubernetes-auth-translator/OidcKubernetesAuthTranslator.test.ts => auth/OidcStrategy.test.ts} (83%) rename plugins/kubernetes-backend/src/{kubernetes-auth-translator/OidcKubernetesAuthTranslator.ts => auth/OidcStrategy.ts} (93%) rename plugins/kubernetes-backend/src/{kubernetes-auth-translator => auth}/index.ts (60%) rename plugins/kubernetes-backend/src/{kubernetes-auth-translator => auth}/types.ts (95%) diff --git a/plugins/kubernetes-backend/api-report.md b/plugins/kubernetes-backend/api-report.md index 37155edc1b..17f1a4e5d9 100644 --- a/plugins/kubernetes-backend/api-report.md +++ b/plugins/kubernetes-backend/api-report.md @@ -22,7 +22,7 @@ import type { RequestHandler } from 'express'; import { TokenCredential } from '@azure/identity'; // @public (undocumented) -export class AksKubernetesAuthTranslator { +export class AksStrategy implements AuthenticationStrategy { // (undocumented) decorateClusterDetailsWithAuth( clusterDetails: ClusterDetails, @@ -31,9 +31,16 @@ export class AksKubernetesAuthTranslator { } // @public (undocumented) -export class AwsIamKubernetesAuthTranslator - implements KubernetesAuthTranslator -{ +export interface AuthenticationStrategy { + // (undocumented) + decorateClusterDetailsWithAuth( + clusterDetails: ClusterDetails, + authConfig: KubernetesRequestAuth, + ): Promise; +} + +// @public (undocumented) +export class AwsIamStrategy implements AuthenticationStrategy { constructor(opts: { config: Config }); // (undocumented) decorateClusterDetailsWithAuth( @@ -42,9 +49,7 @@ export class AwsIamKubernetesAuthTranslator } // @public (undocumented) -export class AzureIdentityKubernetesAuthTranslator - implements KubernetesAuthTranslator -{ +export class AzureIdentityStrategy implements AuthenticationStrategy { constructor(logger: Logger, tokenCredential?: TokenCredential); // (undocumented) decorateClusterDetailsWithAuth( @@ -92,10 +97,8 @@ export interface CustomResourcesByEntity extends KubernetesObjectsByEntity { export const DEFAULT_OBJECTS: ObjectToFetch[]; // @public -export class DispatchingKubernetesAuthTranslator - implements KubernetesAuthTranslator -{ - constructor(options: DispatchingKubernetesAuthTranslatorOptions); +export class DispatchStrategy implements AuthenticationStrategy { + constructor(options: DispatchStrategyOptions); // (undocumented) decorateClusterDetailsWithAuth( clusterDetails: ClusterDetails, @@ -104,9 +107,9 @@ export class DispatchingKubernetesAuthTranslator } // @public (undocumented) -export type DispatchingKubernetesAuthTranslatorOptions = { - authTranslatorMap: { - [key: string]: KubernetesAuthTranslator; +export type DispatchStrategyOptions = { + authStrategyMap: { + [key: string]: AuthenticationStrategy; }; }; @@ -119,23 +122,19 @@ export interface FetchResponseWrapper { } // @public (undocumented) -export class GoogleKubernetesAuthTranslator - implements KubernetesAuthTranslator -{ +export class GoogleServiceAccountStrategy implements AuthenticationStrategy { // (undocumented) decorateClusterDetailsWithAuth( clusterDetails: ClusterDetails, - authConfig: KubernetesRequestAuth, ): Promise; } // @public (undocumented) -export class GoogleServiceAccountAuthTranslator - implements KubernetesAuthTranslator -{ +export class GoogleStrategy implements AuthenticationStrategy { // (undocumented) decorateClusterDetailsWithAuth( clusterDetails: ClusterDetails, + authConfig: KubernetesRequestAuth, ): Promise; } @@ -145,23 +144,14 @@ export const HEADER_KUBERNETES_AUTH: string; // @public export const HEADER_KUBERNETES_CLUSTER: string; -// @public (undocumented) -export interface KubernetesAuthTranslator { - // (undocumented) - decorateClusterDetailsWithAuth( - clusterDetails: ClusterDetails, - authConfig: KubernetesRequestAuth, - ): Promise; -} - // @public (undocumented) export class KubernetesBuilder { constructor(env: KubernetesEnvironment); // (undocumented) build(): KubernetesBuilderReturn; // (undocumented) - protected buildAuthTranslatorMap(): { - [key: string]: KubernetesAuthTranslator; + protected buildAuthStrategyMap(): { + [key: string]: AuthenticationStrategy; }; // (undocumented) protected buildClusterSupplier( @@ -210,8 +200,8 @@ export class KubernetesBuilder { clusterSupplier: KubernetesClustersSupplier, ): Promise; // (undocumented) - protected getAuthTranslatorMap(): { - [key: string]: KubernetesAuthTranslator; + protected getAuthStrategyMap(): { + [key: string]: AuthenticationStrategy; }; // (undocumented) protected getClusterSupplier(): KubernetesClustersSupplier; @@ -233,8 +223,8 @@ export class KubernetesBuilder { // (undocumented) protected getServiceLocatorMethod(): ServiceLocatorMethod; // (undocumented) - setAuthTranslatorMap(authTranslatorMap: { - [key: string]: KubernetesAuthTranslator; + setAuthStrategyMap(authStrategyMap: { + [key: string]: AuthenticationStrategy; }): void; // (undocumented) setClusterSupplier(clusterSupplier?: KubernetesClustersSupplier): this; @@ -259,8 +249,8 @@ export type KubernetesBuilderReturn = Promise<{ proxy: KubernetesProxy; objectsProvider: KubernetesObjectsProvider; serviceLocator: KubernetesServiceLocator; - authTranslatorMap: { - [key: string]: KubernetesAuthTranslator; + authStrategyMap: { + [key: string]: AuthenticationStrategy; }; }>; @@ -365,7 +355,7 @@ export type KubernetesProxyCreateRequestHandlerOptions = { export type KubernetesProxyOptions = { logger: Logger; clusterSupplier: KubernetesClustersSupplier; - authTranslator: KubernetesAuthTranslator; + authStrategy: AuthenticationStrategy; }; // @public @@ -380,7 +370,7 @@ export interface KubernetesServiceLocator { } // @public (undocumented) -export class NoopKubernetesAuthTranslator implements KubernetesAuthTranslator { +export class NoopStrategy implements AuthenticationStrategy { // (undocumented) decorateClusterDetailsWithAuth( clusterDetails: ClusterDetails, @@ -419,7 +409,7 @@ export interface ObjectToFetch { } // @public (undocumented) -export class OidcKubernetesAuthTranslator implements KubernetesAuthTranslator { +export class OidcStrategy implements AuthenticationStrategy { // (undocumented) decorateClusterDetailsWithAuth( clusterDetails: ClusterDetails, diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/AksKubernetesAuthTranslator.test.ts b/plugins/kubernetes-backend/src/auth/AksStrategy.test.ts similarity index 76% rename from plugins/kubernetes-backend/src/kubernetes-auth-translator/AksKubernetesAuthTranslator.test.ts rename to plugins/kubernetes-backend/src/auth/AksStrategy.test.ts index b39377f5a3..47d045d6b2 100644 --- a/plugins/kubernetes-backend/src/kubernetes-auth-translator/AksKubernetesAuthTranslator.test.ts +++ b/plugins/kubernetes-backend/src/auth/AksStrategy.test.ts @@ -13,13 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { AksKubernetesAuthTranslator } from './AksKubernetesAuthTranslator'; +import { AksStrategy } from './AksStrategy'; -describe('AksKubernetesAuthTranslator', () => { +describe('AksStrategy', () => { it('uses auth.aks value as bearer token', async () => { - const translator = new AksKubernetesAuthTranslator(); + const strategy = new AksStrategy(); - const details = await translator.decorateClusterDetailsWithAuth( + const details = await strategy.decorateClusterDetailsWithAuth( { name: '', authProvider: 'aks', url: '' }, { aks: 'aksToken' }, ); diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/AksKubernetesAuthTranslator.ts b/plugins/kubernetes-backend/src/auth/AksStrategy.ts similarity index 90% rename from plugins/kubernetes-backend/src/kubernetes-auth-translator/AksKubernetesAuthTranslator.ts rename to plugins/kubernetes-backend/src/auth/AksStrategy.ts index bdaafc507b..8bd22bcb64 100644 --- a/plugins/kubernetes-backend/src/kubernetes-auth-translator/AksKubernetesAuthTranslator.ts +++ b/plugins/kubernetes-backend/src/auth/AksStrategy.ts @@ -14,13 +14,14 @@ * limitations under the License. */ import { ClusterDetails } from '../types/types'; +import { AuthenticationStrategy } from './types'; import { KubernetesRequestAuth } from '@backstage/plugin-kubernetes-common'; /** * * @public */ -export class AksKubernetesAuthTranslator { +export class AksStrategy implements AuthenticationStrategy { async decorateClusterDetailsWithAuth( clusterDetails: ClusterDetails, auth: KubernetesRequestAuth, diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.test.ts b/plugins/kubernetes-backend/src/auth/AwsIamStrategy.test.ts similarity index 83% rename from plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.test.ts rename to plugins/kubernetes-backend/src/auth/AwsIamStrategy.test.ts index b024283c20..b7182c6c96 100644 --- a/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.test.ts +++ b/plugins/kubernetes-backend/src/auth/AwsIamStrategy.test.ts @@ -18,7 +18,7 @@ import { ANNOTATION_KUBERNETES_AWS_ASSUME_ROLE, ANNOTATION_KUBERNETES_AWS_EXTERNAL_ID, } from '@backstage/plugin-kubernetes-common'; -import { AwsIamKubernetesAuthTranslator } from './AwsIamKubernetesAuthTranslator'; +import { AwsIamStrategy } from './AwsIamStrategy'; let presign = jest.fn(async () => ({ hostname: 'https://example.com', @@ -55,12 +55,12 @@ jest.mock('@aws-sdk/credential-providers', () => ({ }, })); -describe('AwsIamKubernetesAuthTranslator tests', () => { +describe('AwsIamStrategy tests', () => { beforeEach(() => {}); it('returns a signed url for AWS credentials without assume role', async () => { - const authTranslator = new AwsIamKubernetesAuthTranslator({ config }); + const strategy = new AwsIamStrategy({ config }); - const authPromise = authTranslator.decorateClusterDetailsWithAuth({ + const authPromise = strategy.decorateClusterDetailsWithAuth({ name: 'test-cluster', url: '', authProvider: 'aws', @@ -71,9 +71,9 @@ describe('AwsIamKubernetesAuthTranslator tests', () => { }); it('returns a signed url for AWS credentials with assume role', async () => { - const authTranslator = new AwsIamKubernetesAuthTranslator({ config }); + const strategy = new AwsIamStrategy({ config }); - const authPromise = authTranslator.decorateClusterDetailsWithAuth({ + const authPromise = strategy.decorateClusterDetailsWithAuth({ name: 'test-cluster', url: '', authProvider: 'aws', @@ -97,9 +97,9 @@ describe('AwsIamKubernetesAuthTranslator tests', () => { }); it('returns a signed url for AWS credentials and passes the external id', async () => { - const authTranslator = new AwsIamKubernetesAuthTranslator({ config }); + const strategy = new AwsIamStrategy({ config }); - const authPromise = authTranslator.decorateClusterDetailsWithAuth({ + const authPromise = strategy.decorateClusterDetailsWithAuth({ name: 'test-cluster', url: '', authProvider: 'aws', @@ -132,9 +132,9 @@ describe('AwsIamKubernetesAuthTranslator tests', () => { }); }); it('throws the right error', async () => { - const authTranslator = new AwsIamKubernetesAuthTranslator({ config }); + const strategy = new AwsIamStrategy({ config }); await expect( - authTranslator.decorateClusterDetailsWithAuth({ + strategy.decorateClusterDetailsWithAuth({ name: 'test-cluster', url: '', authProvider: 'aws', diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.ts b/plugins/kubernetes-backend/src/auth/AwsIamStrategy.ts similarity index 96% rename from plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.ts rename to plugins/kubernetes-backend/src/auth/AwsIamStrategy.ts index d76ae985de..131535a67b 100644 --- a/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.ts +++ b/plugins/kubernetes-backend/src/auth/AwsIamStrategy.ts @@ -26,7 +26,7 @@ import { ANNOTATION_KUBERNETES_AWS_EXTERNAL_ID, } from '@backstage/plugin-kubernetes-common'; import { ClusterDetails } from '../types/types'; -import { KubernetesAuthTranslator } from './types'; +import { AuthenticationStrategy } from './types'; /** * @@ -44,9 +44,7 @@ const defaultRegion = 'us-east-1'; * * @public */ -export class AwsIamKubernetesAuthTranslator - implements KubernetesAuthTranslator -{ +export class AwsIamStrategy implements AuthenticationStrategy { private readonly credsManager: AwsCredentialsManager; constructor(opts: { config: Config }) { this.credsManager = DefaultAwsCredentialsManager.fromConfig(opts.config); diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/AzureIdentityKubernetesAuthTranslator.test.ts b/plugins/kubernetes-backend/src/auth/AzureIdentityStrategy.test.ts similarity index 70% rename from plugins/kubernetes-backend/src/kubernetes-auth-translator/AzureIdentityKubernetesAuthTranslator.test.ts rename to plugins/kubernetes-backend/src/auth/AzureIdentityStrategy.test.ts index dbc48857f4..2325593271 100644 --- a/plugins/kubernetes-backend/src/kubernetes-auth-translator/AzureIdentityKubernetesAuthTranslator.test.ts +++ b/plugins/kubernetes-backend/src/auth/AzureIdentityStrategy.test.ts @@ -16,7 +16,7 @@ import { AccessToken, TokenCredential } from '@azure/identity'; import { getVoidLogger } from '@backstage/backend-common'; -import { AzureIdentityKubernetesAuthTranslator } from './AzureIdentityKubernetesAuthTranslator'; +import { AzureIdentityStrategy } from './AzureIdentityStrategy'; const logger = getVoidLogger(); @@ -39,7 +39,7 @@ class StaticTokenCredential implements TokenCredential { } } -describe('AzureIdentityKubernetesAuthTranslator tests', () => { +describe('AzureIdentityStrategy tests', () => { const cd = { authProvider: 'Azure', name: 'My Cluster', @@ -51,90 +51,88 @@ describe('AzureIdentityKubernetesAuthTranslator tests', () => { }); it('should decorate cluster with Azure token', async () => { - const authTranslator = new AzureIdentityKubernetesAuthTranslator( + const strategy = new AzureIdentityStrategy( logger, new StaticTokenCredential(5 * 60 * 1000), ); - const response = await authTranslator.decorateClusterDetailsWithAuth(cd); + const response = await strategy.decorateClusterDetailsWithAuth(cd); expect(response.authMetadata!.serviceAccountToken).toEqual('MY_TOKEN_1'); }); it('should re-use token before expiry', async () => { - const authTranslator = new AzureIdentityKubernetesAuthTranslator( + const strategy = new AzureIdentityStrategy( logger, new StaticTokenCredential(20 * 60 * 1000), ); - const response = await authTranslator.decorateClusterDetailsWithAuth(cd); + const response = await strategy.decorateClusterDetailsWithAuth(cd); expect(response.authMetadata!.serviceAccountToken).toEqual('MY_TOKEN_1'); - const response2 = await authTranslator.decorateClusterDetailsWithAuth(cd); + const response2 = await strategy.decorateClusterDetailsWithAuth(cd); expect(response2.authMetadata!.serviceAccountToken).toEqual('MY_TOKEN_1'); }); it('should issue new token 15 minutes befory expiry', async () => { jest.useFakeTimers(); - const authTranslator = new AzureIdentityKubernetesAuthTranslator( + const strategy = new AzureIdentityStrategy( logger, new StaticTokenCredential(16 * 60 * 1000), // token expires in 16min ); - const response = await authTranslator.decorateClusterDetailsWithAuth(cd); + const response = await strategy.decorateClusterDetailsWithAuth(cd); expect(response.authMetadata!.serviceAccountToken).toEqual('MY_TOKEN_1'); jest.setSystemTime(Date.now() + 2 * 60 * 1000); // advance time by 2mins - const response2 = await authTranslator.decorateClusterDetailsWithAuth(cd); + const response2 = await strategy.decorateClusterDetailsWithAuth(cd); expect(response2.authMetadata!.serviceAccountToken).toEqual('MY_TOKEN_2'); }); it('should re-use existing token if there is afailure', async () => { jest.useFakeTimers(); - const authTranslator = new AzureIdentityKubernetesAuthTranslator( + const strategy = new AzureIdentityStrategy( logger, new StaticTokenCredential(16 * 60 * 1000), // new tokens expires in 16min ); - const response = await authTranslator.decorateClusterDetailsWithAuth(cd); + const response = await strategy.decorateClusterDetailsWithAuth(cd); expect(response.authMetadata!.serviceAccountToken).toEqual('MY_TOKEN_1'); jest.setSystemTime(Date.now() + 2 * 60 * 1000); // advance time by 2min - const response2 = await authTranslator.decorateClusterDetailsWithAuth(cd); + const response2 = await strategy.decorateClusterDetailsWithAuth(cd); expect(response2.authMetadata!.serviceAccountToken).toEqual('MY_TOKEN_2'); jest.setSystemTime(Date.now() + 2 * 60 * 1000); // advance time by 2min - const response3 = await authTranslator.decorateClusterDetailsWithAuth(cd); + const response3 = await strategy.decorateClusterDetailsWithAuth(cd); expect(response3.authMetadata!.serviceAccountToken).toEqual('MY_TOKEN_2'); - const response4 = await authTranslator.decorateClusterDetailsWithAuth(cd); + const response4 = await strategy.decorateClusterDetailsWithAuth(cd); expect(response4.authMetadata!.serviceAccountToken).toEqual('MY_TOKEN_4'); }); it('should throw if existing token expired and failed to fetch a new one', async () => { jest.useFakeTimers(); - const authTranslator = new AzureIdentityKubernetesAuthTranslator( + const strategy = new AzureIdentityStrategy( logger, new StaticTokenCredential(16 * 60 * 1000), // new tokens expires in 16min ); - const response = await authTranslator.decorateClusterDetailsWithAuth(cd); + const response = await strategy.decorateClusterDetailsWithAuth(cd); expect(response.authMetadata!.serviceAccountToken).toEqual('MY_TOKEN_1'); jest.setSystemTime(Date.now() + 2 * 60 * 1000); // advance time by 2min - const response2 = await authTranslator.decorateClusterDetailsWithAuth(cd); + const response2 = await strategy.decorateClusterDetailsWithAuth(cd); expect(response2.authMetadata!.serviceAccountToken).toEqual('MY_TOKEN_2'); jest.setSystemTime(Date.now() + 17 * 60 * 1000); // advance time by 17min - await expect( - authTranslator.decorateClusterDetailsWithAuth(cd), - ).rejects.toThrow(); + await expect(strategy.decorateClusterDetailsWithAuth(cd)).rejects.toThrow(); }); }); diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/AzureIdentityKubernetesAuthTranslator.ts b/plugins/kubernetes-backend/src/auth/AzureIdentityStrategy.ts similarity index 95% rename from plugins/kubernetes-backend/src/kubernetes-auth-translator/AzureIdentityKubernetesAuthTranslator.ts rename to plugins/kubernetes-backend/src/auth/AzureIdentityStrategy.ts index 3e0622a9d2..038100a19f 100644 --- a/plugins/kubernetes-backend/src/kubernetes-auth-translator/AzureIdentityKubernetesAuthTranslator.ts +++ b/plugins/kubernetes-backend/src/auth/AzureIdentityStrategy.ts @@ -15,7 +15,7 @@ */ import { Logger } from 'winston'; -import { KubernetesAuthTranslator } from './types'; +import { AuthenticationStrategy } from './types'; import { ClusterDetails } from '../types/types'; import { AccessToken, @@ -29,9 +29,7 @@ const aksScope = '6dae42f8-4368-4678-94ff-3960e28e3630/.default'; // This scope * * @public */ -export class AzureIdentityKubernetesAuthTranslator - implements KubernetesAuthTranslator -{ +export class AzureIdentityStrategy implements AuthenticationStrategy { private accessToken: AccessToken = { token: '', expiresOnTimestamp: 0 }; private newTokenPromise: Promise | undefined; diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/DispatchingKubernetesAuthTranslator.test.ts b/plugins/kubernetes-backend/src/auth/DispatchStrategy.test.ts similarity index 60% rename from plugins/kubernetes-backend/src/kubernetes-auth-translator/DispatchingKubernetesAuthTranslator.test.ts rename to plugins/kubernetes-backend/src/auth/DispatchStrategy.test.ts index be41c593f2..d53f9f1e6c 100644 --- a/plugins/kubernetes-backend/src/kubernetes-auth-translator/DispatchingKubernetesAuthTranslator.test.ts +++ b/plugins/kubernetes-backend/src/auth/DispatchStrategy.test.ts @@ -14,50 +14,50 @@ * limitations under the License. */ -import { DispatchingKubernetesAuthTranslator } from './DispatchingKubernetesAuthTranslator'; +import { DispatchStrategy } from './DispatchStrategy'; import { ClusterDetails } from '../types'; import { KubernetesRequestAuth } from '@backstage/plugin-kubernetes-common'; -import { KubernetesAuthTranslator } from './types'; +import { AuthenticationStrategy } from './types'; describe('decorateClusterDetailsWithAuth', () => { - let authTranslator: DispatchingKubernetesAuthTranslator; - let mockTranslator: jest.Mocked; + let strategy: DispatchStrategy; + let mockStrategy: jest.Mocked; const authObject: KubernetesRequestAuth = {}; beforeEach(() => { - mockTranslator = { decorateClusterDetailsWithAuth: jest.fn() }; - authTranslator = new DispatchingKubernetesAuthTranslator({ - authTranslatorMap: { google: mockTranslator }, + mockStrategy = { decorateClusterDetailsWithAuth: jest.fn() }; + strategy = new DispatchStrategy({ + authStrategyMap: { google: mockStrategy }, }); }); - it('can decorate cluster details if the auth provider is in the translator map', async () => { + it('can decorate cluster details if the auth provider is in the strategy map', async () => { const expectedClusterDetails: ClusterDetails = { url: 'notanything.com', name: 'randomName', authProvider: 'google', - authMetadata: { serviceAccountToken: 'added by mock translator' }, + authMetadata: { serviceAccountToken: 'added by mock strategy' }, }; - mockTranslator.decorateClusterDetailsWithAuth.mockResolvedValue( + mockStrategy.decorateClusterDetailsWithAuth.mockResolvedValue( expectedClusterDetails, ); - const returnedValue = await authTranslator.decorateClusterDetailsWithAuth( + const returnedValue = await strategy.decorateClusterDetailsWithAuth( { name: 'googleCluster', url: 'anything.com', authProvider: 'google' }, authObject, ); - expect(mockTranslator.decorateClusterDetailsWithAuth).toHaveBeenCalledWith( + expect(mockStrategy.decorateClusterDetailsWithAuth).toHaveBeenCalledWith( { name: 'googleCluster', url: 'anything.com', authProvider: 'google' }, authObject, ); expect(returnedValue).toBe(expectedClusterDetails); }); - it('throws an error when asked for an auth translator for an unsupported auth type', () => { + it('throws an error when asked for a strategy for an unsupported auth type', () => { expect(() => - authTranslator.decorateClusterDetailsWithAuth( + strategy.decorateClusterDetailsWithAuth( { name: 'test-cluster', url: 'anything.com', @@ -66,7 +66,7 @@ describe('decorateClusterDetailsWithAuth', () => { authObject, ), ).toThrow( - 'authProvider "linode" has no KubernetesAuthTranslator associated with it', + 'authProvider "linode" has no AuthenticationStrategy associated with it', ); }); }); diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/DispatchingKubernetesAuthTranslator.ts b/plugins/kubernetes-backend/src/auth/DispatchStrategy.ts similarity index 61% rename from plugins/kubernetes-backend/src/kubernetes-auth-translator/DispatchingKubernetesAuthTranslator.ts rename to plugins/kubernetes-backend/src/auth/DispatchStrategy.ts index 82d9538cac..c40ff66d27 100644 --- a/plugins/kubernetes-backend/src/kubernetes-auth-translator/DispatchingKubernetesAuthTranslator.ts +++ b/plugins/kubernetes-backend/src/auth/DispatchStrategy.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { KubernetesAuthTranslator } from './types'; +import { AuthenticationStrategy } from './types'; import { ClusterDetails } from '../types'; import { KubernetesRequestAuth } from '@backstage/plugin-kubernetes-common'; @@ -22,35 +22,33 @@ import { KubernetesRequestAuth } from '@backstage/plugin-kubernetes-common'; * * @public */ -export type DispatchingKubernetesAuthTranslatorOptions = { - authTranslatorMap: { - [key: string]: KubernetesAuthTranslator; +export type DispatchStrategyOptions = { + authStrategyMap: { + [key: string]: AuthenticationStrategy; }; }; /** - * used to direct a KubernetesAuthProvider to its corresponding KubernetesAuthTranslator + * used to direct a KubernetesAuthProvider to its corresponding AuthenticationStrategy * @public */ -export class DispatchingKubernetesAuthTranslator - implements KubernetesAuthTranslator -{ - private readonly translatorMap: { [key: string]: KubernetesAuthTranslator }; +export class DispatchStrategy implements AuthenticationStrategy { + private readonly strategyMap: { [key: string]: AuthenticationStrategy }; - constructor(options: DispatchingKubernetesAuthTranslatorOptions) { - this.translatorMap = options.authTranslatorMap; + constructor(options: DispatchStrategyOptions) { + this.strategyMap = options.authStrategyMap; } public decorateClusterDetailsWithAuth( clusterDetails: ClusterDetails, auth: KubernetesRequestAuth, ) { - if (this.translatorMap[clusterDetails.authProvider]) { - return this.translatorMap[ + if (this.strategyMap[clusterDetails.authProvider]) { + return this.strategyMap[ clusterDetails.authProvider ].decorateClusterDetailsWithAuth(clusterDetails, auth); } throw new Error( - `authProvider "${clusterDetails.authProvider}" has no KubernetesAuthTranslator associated with it`, + `authProvider "${clusterDetails.authProvider}" has no AuthenticationStrategy associated with it`, ); } } diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/GoogleServiceAccountAuthProvider.ts b/plugins/kubernetes-backend/src/auth/GoogleServiceAccountStrategy.ts similarity index 91% rename from plugins/kubernetes-backend/src/kubernetes-auth-translator/GoogleServiceAccountAuthProvider.ts rename to plugins/kubernetes-backend/src/auth/GoogleServiceAccountStrategy.ts index 8aa60e0012..db8d9d2c2c 100644 --- a/plugins/kubernetes-backend/src/kubernetes-auth-translator/GoogleServiceAccountAuthProvider.ts +++ b/plugins/kubernetes-backend/src/auth/GoogleServiceAccountStrategy.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { KubernetesAuthTranslator } from './types'; +import { AuthenticationStrategy } from './types'; import { ClusterDetails } from '../types/types'; import * as container from '@google-cloud/container'; @@ -21,9 +21,7 @@ import * as container from '@google-cloud/container'; * * @public */ -export class GoogleServiceAccountAuthTranslator - implements KubernetesAuthTranslator -{ +export class GoogleServiceAccountStrategy implements AuthenticationStrategy { async decorateClusterDetailsWithAuth( clusterDetails: ClusterDetails, ): Promise { diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/GoogleKubernetesAuthTranslator.ts b/plugins/kubernetes-backend/src/auth/GoogleStrategy.ts similarity index 91% rename from plugins/kubernetes-backend/src/kubernetes-auth-translator/GoogleKubernetesAuthTranslator.ts rename to plugins/kubernetes-backend/src/auth/GoogleStrategy.ts index d0bef9c21b..c14f85051c 100644 --- a/plugins/kubernetes-backend/src/kubernetes-auth-translator/GoogleKubernetesAuthTranslator.ts +++ b/plugins/kubernetes-backend/src/auth/GoogleStrategy.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { KubernetesAuthTranslator } from './types'; +import { AuthenticationStrategy } from './types'; import { ClusterDetails } from '../types/types'; import { KubernetesRequestAuth } from '@backstage/plugin-kubernetes-common'; @@ -22,9 +22,7 @@ import { KubernetesRequestAuth } from '@backstage/plugin-kubernetes-common'; * * @public */ -export class GoogleKubernetesAuthTranslator - implements KubernetesAuthTranslator -{ +export class GoogleStrategy implements AuthenticationStrategy { async decorateClusterDetailsWithAuth( clusterDetails: ClusterDetails, authConfig: KubernetesRequestAuth, diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/NoopKubernetesAuthTranslator.ts b/plugins/kubernetes-backend/src/auth/NoopStrategy.ts similarity index 86% rename from plugins/kubernetes-backend/src/kubernetes-auth-translator/NoopKubernetesAuthTranslator.ts rename to plugins/kubernetes-backend/src/auth/NoopStrategy.ts index a44710f58b..c96d2a55c7 100644 --- a/plugins/kubernetes-backend/src/kubernetes-auth-translator/NoopKubernetesAuthTranslator.ts +++ b/plugins/kubernetes-backend/src/auth/NoopStrategy.ts @@ -14,14 +14,14 @@ * limitations under the License. */ -import { KubernetesAuthTranslator } from './types'; +import { AuthenticationStrategy } from './types'; import { ClusterDetails } from '../types/types'; /** * * @public */ -export class NoopKubernetesAuthTranslator implements KubernetesAuthTranslator { +export class NoopStrategy implements AuthenticationStrategy { async decorateClusterDetailsWithAuth( clusterDetails: ClusterDetails, ): Promise { diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/OidcKubernetesAuthTranslator.test.ts b/plugins/kubernetes-backend/src/auth/OidcStrategy.test.ts similarity index 83% rename from plugins/kubernetes-backend/src/kubernetes-auth-translator/OidcKubernetesAuthTranslator.test.ts rename to plugins/kubernetes-backend/src/auth/OidcStrategy.test.ts index 2641922b3a..1160308521 100644 --- a/plugins/kubernetes-backend/src/kubernetes-auth-translator/OidcKubernetesAuthTranslator.test.ts +++ b/plugins/kubernetes-backend/src/auth/OidcStrategy.test.ts @@ -15,11 +15,11 @@ */ import { ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER } from '@backstage/plugin-kubernetes-common'; -import { OidcKubernetesAuthTranslator } from './OidcKubernetesAuthTranslator'; +import { OidcStrategy } from './OidcStrategy'; import { ClusterDetails } from '../types/types'; -describe('OidcKubernetesAuthTranslator tests', () => { - const at = new OidcKubernetesAuthTranslator(); +describe('OidcStrategy tests', () => { + const strategy = new OidcStrategy(); const baseClusterDetails: ClusterDetails = { name: 'test', authProvider: 'oidc', @@ -27,7 +27,7 @@ describe('OidcKubernetesAuthTranslator tests', () => { }; it('returns cluster details with auth token', async () => { - const details = await at.decorateClusterDetailsWithAuth( + const details = await strategy.decorateClusterDetailsWithAuth( { authMetadata: { [ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER]: 'okta' }, ...baseClusterDetails, @@ -42,7 +42,7 @@ describe('OidcKubernetesAuthTranslator tests', () => { it('returns error when oidcTokenProvider is not configured', async () => { await expect( - at.decorateClusterDetailsWithAuth(baseClusterDetails, {}), + strategy.decorateClusterDetailsWithAuth(baseClusterDetails, {}), ).rejects.toThrow( 'oidc authProvider requires a configured oidcTokenProvider', ); @@ -50,7 +50,7 @@ describe('OidcKubernetesAuthTranslator tests', () => { it('returns error when token is not included in request body', async () => { await expect( - at.decorateClusterDetailsWithAuth( + strategy.decorateClusterDetailsWithAuth( { authMetadata: { [ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER]: 'okta' }, ...baseClusterDetails, diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/OidcKubernetesAuthTranslator.ts b/plugins/kubernetes-backend/src/auth/OidcStrategy.ts similarity index 93% rename from plugins/kubernetes-backend/src/kubernetes-auth-translator/OidcKubernetesAuthTranslator.ts rename to plugins/kubernetes-backend/src/auth/OidcStrategy.ts index ef13bfb146..db41ba6776 100644 --- a/plugins/kubernetes-backend/src/kubernetes-auth-translator/OidcKubernetesAuthTranslator.ts +++ b/plugins/kubernetes-backend/src/auth/OidcStrategy.ts @@ -17,14 +17,14 @@ import { ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER, KubernetesRequestAuth, } from '@backstage/plugin-kubernetes-common'; -import { KubernetesAuthTranslator } from './types'; +import { AuthenticationStrategy } from './types'; import { ClusterDetails } from '../types/types'; /** * * @public */ -export class OidcKubernetesAuthTranslator implements KubernetesAuthTranslator { +export class OidcStrategy implements AuthenticationStrategy { async decorateClusterDetailsWithAuth( clusterDetails: ClusterDetails, authConfig: KubernetesRequestAuth, diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/index.ts b/plugins/kubernetes-backend/src/auth/index.ts similarity index 60% rename from plugins/kubernetes-backend/src/kubernetes-auth-translator/index.ts rename to plugins/kubernetes-backend/src/auth/index.ts index 71dcedfee6..2caefc3a12 100644 --- a/plugins/kubernetes-backend/src/kubernetes-auth-translator/index.ts +++ b/plugins/kubernetes-backend/src/auth/index.ts @@ -14,12 +14,12 @@ * limitations under the License. */ -export * from './AksKubernetesAuthTranslator'; -export * from './AwsIamKubernetesAuthTranslator'; -export * from './AzureIdentityKubernetesAuthTranslator'; -export * from './GoogleKubernetesAuthTranslator'; -export * from './GoogleServiceAccountAuthProvider'; -export * from './DispatchingKubernetesAuthTranslator'; -export * from './NoopKubernetesAuthTranslator'; -export * from './OidcKubernetesAuthTranslator'; +export * from './AksStrategy'; +export * from './AwsIamStrategy'; +export * from './AzureIdentityStrategy'; +export * from './GoogleStrategy'; +export * from './GoogleServiceAccountStrategy'; +export * from './DispatchStrategy'; +export * from './NoopStrategy'; +export * from './OidcStrategy'; export * from './types'; diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/types.ts b/plugins/kubernetes-backend/src/auth/types.ts similarity index 95% rename from plugins/kubernetes-backend/src/kubernetes-auth-translator/types.ts rename to plugins/kubernetes-backend/src/auth/types.ts index efbbff8250..79a581bfd0 100644 --- a/plugins/kubernetes-backend/src/kubernetes-auth-translator/types.ts +++ b/plugins/kubernetes-backend/src/auth/types.ts @@ -21,7 +21,7 @@ import { KubernetesRequestAuth } from '@backstage/plugin-kubernetes-common'; * * @public */ -export interface KubernetesAuthTranslator { +export interface AuthenticationStrategy { decorateClusterDetailsWithAuth( clusterDetails: ClusterDetails, authConfig: KubernetesRequestAuth, diff --git a/plugins/kubernetes-backend/src/index.ts b/plugins/kubernetes-backend/src/index.ts index 67aefbd974..d32dfc24be 100644 --- a/plugins/kubernetes-backend/src/index.ts +++ b/plugins/kubernetes-backend/src/index.ts @@ -20,6 +20,6 @@ * @packageDocumentation */ -export * from './kubernetes-auth-translator'; +export * from './auth'; export * from './service'; export * from './types'; diff --git a/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts b/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts index 905ef521a3..02ea54a1d5 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts @@ -25,16 +25,16 @@ import { Logger } from 'winston'; import { getCombinedClusterSupplier } from '../cluster-locator'; import { - KubernetesAuthTranslator, - DispatchingKubernetesAuthTranslator, - GoogleKubernetesAuthTranslator, - NoopKubernetesAuthTranslator, - AwsIamKubernetesAuthTranslator, - GoogleServiceAccountAuthTranslator, - AzureIdentityKubernetesAuthTranslator, - OidcKubernetesAuthTranslator, - AksKubernetesAuthTranslator, -} from '../kubernetes-auth-translator'; + AuthenticationStrategy, + DispatchStrategy, + GoogleStrategy, + NoopStrategy, + AwsIamStrategy, + GoogleServiceAccountStrategy, + AzureIdentityStrategy, + OidcStrategy, + AksStrategy, +} from '../auth'; import { addResourceRoutesToRouter } from '../routes/resourcesRoutes'; import { MultiTenantServiceLocator } from '../service-locator/MultiTenantServiceLocator'; @@ -80,7 +80,7 @@ export type KubernetesBuilderReturn = Promise<{ proxy: KubernetesProxy; objectsProvider: KubernetesObjectsProvider; serviceLocator: KubernetesServiceLocator; - authTranslatorMap: { [key: string]: KubernetesAuthTranslator }; + authStrategyMap: { [key: string]: AuthenticationStrategy }; }>; /** @@ -96,7 +96,7 @@ export class KubernetesBuilder { private fetcher?: KubernetesFetcher; private serviceLocator?: KubernetesServiceLocator; private proxy?: KubernetesProxy; - private authTranslatorMap?: { [key: string]: KubernetesAuthTranslator }; + private authStrategyMap?: { [key: string]: AuthenticationStrategy }; static createBuilder(env: KubernetesEnvironment) { return new KubernetesBuilder(env); @@ -128,7 +128,7 @@ export class KubernetesBuilder { const clusterSupplier = this.getClusterSupplier(); - const authTranslatorMap = this.getAuthTranslatorMap(); + const authStrategyMap = this.getAuthStrategyMap(); const proxy = this.getProxy(logger, clusterSupplier); @@ -159,7 +159,7 @@ export class KubernetesBuilder { objectsProvider, router, serviceLocator, - authTranslatorMap, + authStrategyMap, }; } @@ -193,10 +193,10 @@ export class KubernetesBuilder { return this; } - public setAuthTranslatorMap(authTranslatorMap: { - [key: string]: KubernetesAuthTranslator; + public setAuthStrategyMap(authStrategyMap: { + [key: string]: AuthenticationStrategy; }) { - this.authTranslatorMap = authTranslatorMap; + this.authStrategyMap = authStrategyMap; } protected buildCustomResources() { @@ -234,11 +234,11 @@ export class KubernetesBuilder { protected buildObjectsProvider( options: KubernetesObjectsProviderOptions, ): KubernetesObjectsProvider { - const authTranslatorMap = this.getAuthTranslatorMap(); + const authStrategyMap = this.getAuthStrategyMap(); this.objectsProvider = new KubernetesFanOutHandler({ ...options, - authTranslator: new DispatchingKubernetesAuthTranslator({ - authTranslatorMap, + authStrategy: new DispatchStrategy({ + authStrategyMap, }), }); @@ -290,14 +290,14 @@ export class KubernetesBuilder { logger: Logger, clusterSupplier: KubernetesClustersSupplier, ): KubernetesProxy { - const authTranslatorMap = this.getAuthTranslatorMap(); - const authTranslator = new DispatchingKubernetesAuthTranslator({ - authTranslatorMap, + const authStrategyMap = this.getAuthStrategyMap(); + const authStrategy = new DispatchStrategy({ + authStrategyMap, }); this.proxy = new KubernetesProxy({ logger, clusterSupplier, - authTranslator, + authStrategy, }); return this.proxy; } @@ -356,18 +356,18 @@ export class KubernetesBuilder { return router; } - protected buildAuthTranslatorMap() { - this.authTranslatorMap = { - google: new GoogleKubernetesAuthTranslator(), - aks: new AksKubernetesAuthTranslator(), - aws: new AwsIamKubernetesAuthTranslator({ config: this.env.config }), - azure: new AzureIdentityKubernetesAuthTranslator(this.env.logger), - serviceAccount: new NoopKubernetesAuthTranslator(), - googleServiceAccount: new GoogleServiceAccountAuthTranslator(), - oidc: new OidcKubernetesAuthTranslator(), - localKubectlProxy: new NoopKubernetesAuthTranslator(), + protected buildAuthStrategyMap() { + this.authStrategyMap = { + google: new GoogleStrategy(), + aks: new AksStrategy(), + aws: new AwsIamStrategy({ config: this.env.config }), + azure: new AzureIdentityStrategy(this.env.logger), + serviceAccount: new NoopStrategy(), + googleServiceAccount: new GoogleServiceAccountStrategy(), + oidc: new OidcStrategy(), + localKubectlProxy: new NoopStrategy(), }; - return this.authTranslatorMap; + return this.authStrategyMap; } protected async fetchClusterDetails( @@ -450,7 +450,7 @@ export class KubernetesBuilder { return this.proxy ?? this.buildProxy(logger, clusterSupplier); } - protected getAuthTranslatorMap() { - return this.authTranslatorMap ?? this.buildAuthTranslatorMap(); + protected getAuthStrategyMap() { + return this.authStrategyMap ?? this.buildAuthStrategyMap(); } } diff --git a/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.test.ts b/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.test.ts index e092ac082c..1748beb006 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.test.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.test.ts @@ -179,7 +179,7 @@ describe('KubernetesFanOutHandler', () => { getClustersByEntity, }, customResources: customResources, - authTranslator: { + authStrategy: { decorateClusterDetailsWithAuth: async (clusterDetails, _) => { return clusterDetails; }, @@ -1100,7 +1100,7 @@ describe('KubernetesFanOutHandler', () => { objectType: 'services', }, ], - authTranslator: { + authStrategy: { decorateClusterDetailsWithAuth: async (clusterDetails, _) => { return clusterDetails; }, diff --git a/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.ts b/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.ts index 79003ae1ca..98ad41947b 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.ts @@ -29,7 +29,7 @@ import { KubernetesObjectsByEntity, ServiceLocatorRequestContext, } from '../types/types'; -import { KubernetesAuthTranslator } from '../kubernetes-auth-translator/types'; +import { AuthenticationStrategy } from '../auth/types'; import { ClientContainerStatus, ClientCurrentResourceUsage, @@ -137,7 +137,7 @@ export const DEFAULT_OBJECTS: ObjectToFetch[] = [ export interface KubernetesFanOutHandlerOptions extends KubernetesObjectsProviderOptions { - authTranslator: KubernetesAuthTranslator; + authStrategy: AuthenticationStrategy; } export interface KubernetesRequestBody extends ObjectsByEntityRequest {} @@ -196,7 +196,7 @@ export class KubernetesFanOutHandler { private readonly serviceLocator: KubernetesServiceLocator; private readonly customResources: CustomResource[]; private readonly objectTypesToFetch: Set; - private readonly authTranslator: KubernetesAuthTranslator; + private readonly authStrategy: AuthenticationStrategy; constructor({ logger, @@ -204,14 +204,14 @@ export class KubernetesFanOutHandler { serviceLocator, customResources, objectTypesToFetch = DEFAULT_OBJECTS, - authTranslator, + authStrategy, }: KubernetesFanOutHandlerOptions) { this.logger = logger; this.fetcher = fetcher; this.serviceLocator = serviceLocator; this.customResources = customResources; this.objectTypesToFetch = new Set(objectTypesToFetch); - this.authTranslator = authTranslator; + this.authStrategy = authStrategy; } async getCustomResourcesByEntity({ @@ -317,7 +317,7 @@ export class KubernetesFanOutHandler { // Execute all of these async actions simultaneously/without blocking sequentially as no common object is modified by them const promiseResults = await Promise.allSettled( clusterDetails.map(cd => { - return this.authTranslator.decorateClusterDetailsWithAuth(cd, auth); + return this.authStrategy.decorateClusterDetailsWithAuth(cd, auth); }), ); diff --git a/plugins/kubernetes-backend/src/service/KubernetesProxy.test.ts b/plugins/kubernetes-backend/src/service/KubernetesProxy.test.ts index 8f25b217ab..6fc5c26620 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesProxy.test.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesProxy.test.ts @@ -33,10 +33,7 @@ import request from 'supertest'; import { AddressInfo, WebSocket, WebSocketServer } from 'ws'; import { LocalKubectlProxyClusterLocator } from '../cluster-locator/LocalKubectlProxyLocator'; -import { - KubernetesAuthTranslator, - NoopKubernetesAuthTranslator, -} from '../kubernetes-auth-translator'; +import { AuthenticationStrategy, NoopStrategy } from '../auth'; import { ClusterDetails, KubernetesClustersSupplier } from '../types/types'; import { APPLICATION_JSON, @@ -62,7 +59,7 @@ describe('KubernetesProxy', () => { authorizeConditional: jest.fn(), }; - const authTranslator: jest.Mocked = { + const authStrategy: jest.Mocked = { decorateClusterDetailsWithAuth: jest.fn(), }; @@ -126,7 +123,7 @@ describe('KubernetesProxy', () => { beforeEach(() => { jest.resetAllMocks(); - proxy = new KubernetesProxy({ logger, clusterSupplier, authTranslator }); + proxy = new KubernetesProxy({ logger, clusterSupplier, authStrategy }); permissionApi.authorize.mockResolvedValue([ { result: AuthorizeResult.ALLOW }, ]); @@ -205,7 +202,7 @@ describe('KubernetesProxy', () => { }, ] as ClusterDetails[]); - authTranslator.decorateClusterDetailsWithAuth.mockResolvedValue({ + authStrategy.decorateClusterDetailsWithAuth.mockResolvedValue({ name: 'cluster1', url: 'https://localhost:9999', authProvider: 'serviceAccount', @@ -249,7 +246,7 @@ describe('KubernetesProxy', () => { }, ] as ClusterDetails[]); - authTranslator.decorateClusterDetailsWithAuth.mockResolvedValue({ + authStrategy.decorateClusterDetailsWithAuth.mockResolvedValue({ name: 'cluster1', url: 'https://localhost:9999', authProvider: 'serviceAccount', @@ -292,7 +289,7 @@ describe('KubernetesProxy', () => { authProvider: '', }, ]); - authTranslator.decorateClusterDetailsWithAuth.mockImplementation( + authStrategy.decorateClusterDetailsWithAuth.mockImplementation( async x => x, ); @@ -308,7 +305,7 @@ describe('KubernetesProxy', () => { expect(response.status).toEqual(200); }); - it('should default to using a authTranslator provided serviceAccountToken as authorization headers to kubeapi when backstage-kubernetes-auth field is not provided', async () => { + it('should default to using an authStrategy-provided serviceAccountToken as authorization headers to kubeapi when backstage-kubernetes-auth field is not provided', async () => { worker.use( rest.get( 'https://localhost:9999/api/v1/namespaces', @@ -319,7 +316,7 @@ describe('KubernetesProxy', () => { if ( req.headers.get('Authorization') !== - 'Bearer translator-provided-token' + 'Bearer strategy-provided-token' ) { return res(ctx.status(403)); } @@ -344,11 +341,11 @@ describe('KubernetesProxy', () => { }, ] as ClusterDetails[]); - authTranslator.decorateClusterDetailsWithAuth.mockResolvedValue({ + authStrategy.decorateClusterDetailsWithAuth.mockResolvedValue({ name: 'cluster1', url: 'https://localhost:9999', authProvider: 'serviceAccount', - authMetadata: { serviceAccountToken: 'translator-provided-token' }, + authMetadata: { serviceAccountToken: 'strategy-provided-token' }, } as ClusterDetails); const requestPromise = setupProxyPromise({ @@ -363,7 +360,7 @@ describe('KubernetesProxy', () => { expect(response.status).toEqual(200); }); - it('should add a authTranslator provided serviceAccountToken as authorization headers to kubeapi if one isnt provided in request and one isnt set up in cluster details', async () => { + it('should add an authStrategy-provided serviceAccountToken as authorization headers to kubeapi if one isnt provided in request and one isnt set up in cluster details', async () => { worker.use( rest.get('https://localhost:9999/api/v1/namespaces', (req, res, ctx) => { if (!req.headers.get('Authorization')) { @@ -393,7 +390,7 @@ describe('KubernetesProxy', () => { }, ] as ClusterDetails[]); - authTranslator.decorateClusterDetailsWithAuth.mockResolvedValue({ + authStrategy.decorateClusterDetailsWithAuth.mockResolvedValue({ name: 'cluster1', url: 'https://localhost:9999', authProvider: 'googleServiceAccount', @@ -447,7 +444,7 @@ describe('KubernetesProxy', () => { }, ] as ClusterDetails[]); - authTranslator.decorateClusterDetailsWithAuth.mockResolvedValue({ + authStrategy.decorateClusterDetailsWithAuth.mockResolvedValue({ name: 'cluster1', url: 'https://localhost:9999', authProvider: 'googleServiceAccount', @@ -474,7 +471,7 @@ describe('KubernetesProxy', () => { }); }); - it('should not invoke authTranslator if Backstage-Kubernetes-Authorization field is provided', async () => { + it('should not invoke authStrategy if Backstage-Kubernetes-Authorization field is provided', async () => { worker.use( rest.get('https://localhost:9999/api/v1/namespaces', (req, res, ctx) => { if (!req.headers.get('Authorization')) { @@ -516,7 +513,7 @@ describe('KubernetesProxy', () => { const response = await requestPromise; - expect(authTranslator.decorateClusterDetailsWithAuth).toHaveBeenCalledTimes( + expect(authStrategy.decorateClusterDetailsWithAuth).toHaveBeenCalledTimes( 0, ); expect(response.status).toEqual(200); @@ -531,7 +528,7 @@ describe('KubernetesProxy', () => { proxy = new KubernetesProxy({ logger: getVoidLogger(), clusterSupplier: new LocalKubectlProxyClusterLocator(), - authTranslator: new NoopKubernetesAuthTranslator(), + authStrategy: new NoopStrategy(), }); worker.use( @@ -568,7 +565,7 @@ describe('KubernetesProxy', () => { }); }); - it('returns a 500 error if authTranslator errors out and Backstage-Kubernetes-Authorization field is not provided', async () => { + it('returns a 500 error if authStrategy errors out and Backstage-Kubernetes-Authorization field is not provided', async () => { worker.use( rest.get('https://localhost:9999/api/v1/namespaces', (req, res, ctx) => { if (!req.headers.get('Authorization')) { @@ -599,7 +596,7 @@ describe('KubernetesProxy', () => { }, ] as ClusterDetails[]); - authTranslator.decorateClusterDetailsWithAuth.mockRejectedValue( + authStrategy.decorateClusterDetailsWithAuth.mockRejectedValue( Error('some internal error'), ); @@ -642,7 +639,7 @@ describe('KubernetesProxy', () => { }, ]); - authTranslator.decorateClusterDetailsWithAuth.mockImplementation( + authStrategy.decorateClusterDetailsWithAuth.mockImplementation( async x => x, ); @@ -787,7 +784,7 @@ describe('KubernetesProxy', () => { }, ] as ClusterDetails[]); - authTranslator.decorateClusterDetailsWithAuth.mockResolvedValue({ + authStrategy.decorateClusterDetailsWithAuth.mockResolvedValue({ name: 'local', url: `http://localhost:${wsPort}`, authProvider: 'serviceAccount', diff --git a/plugins/kubernetes-backend/src/service/KubernetesProxy.ts b/plugins/kubernetes-backend/src/service/KubernetesProxy.ts index e3fc293436..07753a2517 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesProxy.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesProxy.ts @@ -31,7 +31,7 @@ import { bufferFromFileOrString } from '@kubernetes/client-node'; import type { Request, RequestHandler } from 'express'; import { createProxyMiddleware } from 'http-proxy-middleware'; import { Logger } from 'winston'; -import { KubernetesAuthTranslator } from '../kubernetes-auth-translator'; +import { AuthenticationStrategy } from '../auth'; import { ClusterDetails, KubernetesClustersSupplier } from '../types/types'; export const APPLICATION_JSON: string = 'application/json'; @@ -68,7 +68,7 @@ export type KubernetesProxyCreateRequestHandlerOptions = { export type KubernetesProxyOptions = { logger: Logger; clusterSupplier: KubernetesClustersSupplier; - authTranslator: KubernetesAuthTranslator; + authStrategy: AuthenticationStrategy; }; /** @@ -80,12 +80,12 @@ export class KubernetesProxy { private readonly middlewareForClusterName = new Map(); private readonly logger: Logger; private readonly clusterSupplier: KubernetesClustersSupplier; - private readonly authTranslator: KubernetesAuthTranslator; + private readonly authStrategy: AuthenticationStrategy; constructor(options: KubernetesProxyOptions) { this.logger = options.logger; this.clusterSupplier = options.clusterSupplier; - this.authTranslator = options.authTranslator; + this.authStrategy = options.authStrategy; } public createRequestHandler( @@ -113,9 +113,7 @@ export class KubernetesProxy { req.headers.authorization = authHeader; } else { const serviceAccountToken = await this.getClusterForRequest(req) - .then(cd => - this.authTranslator.decorateClusterDetailsWithAuth(cd, {}), - ) + .then(cd => this.authStrategy.decorateClusterDetailsWithAuth(cd, {})) .then(cd => cd?.authMetadata?.serviceAccountToken); if (serviceAccountToken) { req.headers.authorization = `Bearer ${serviceAccountToken}`; From ce6905f86df53bc304532ad7a483b9a8885c5f05 Mon Sep 17 00:00:00 2001 From: Jamie Klassen Date: Wed, 6 Sep 2023 19:55:46 -0400 Subject: [PATCH 11/20] strategies validate metadata And migrate authProvider into metadata. Also thread a DispatchStrategy into ConfigClusterLocator instead of a map of validator functions. Signed-off-by: Jamie Klassen --- plugins/kubernetes-backend/api-report.md | 24 ++++- .../src/auth/AksStrategy.test.ts | 4 +- .../src/auth/AksStrategy.ts | 5 +- .../src/auth/AwsIamStrategy.test.ts | 18 ++-- .../src/auth/AwsIamStrategy.ts | 43 +++++---- .../src/auth/AzureIdentityStrategy.test.ts | 24 ++--- .../src/auth/AzureIdentityStrategy.ts | 6 +- .../src/auth/DispatchStrategy.test.ts | 25 +++-- .../src/auth/DispatchStrategy.ts | 24 +++-- .../src/auth/GoogleServiceAccountStrategy.ts | 6 +- .../src/auth/GoogleStrategy.ts | 5 +- .../src/auth/NoopStrategy.ts | 6 +- .../src/auth/OidcStrategy.test.ts | 33 ++++--- .../src/auth/OidcStrategy.ts | 14 ++- plugins/kubernetes-backend/src/auth/types.ts | 3 +- .../CatalogClusterLocator.test.ts | 4 +- .../cluster-locator/CatalogClusterLocator.ts | 12 ++- .../ConfigClusterLocator.test.ts | 67 +++++++++----- .../cluster-locator/ConfigClusterLocator.ts | 92 ++++++++++--------- .../cluster-locator/GkeClusterLocator.test.ts | 16 ++-- .../src/cluster-locator/GkeClusterLocator.ts | 2 +- .../LocalKubectlProxyLocator.ts | 2 +- .../src/cluster-locator/index.test.ts | 58 ++++++------ .../src/cluster-locator/index.ts | 7 +- .../MultiTenantServiceLocator.test.ts | 30 +++--- .../src/service/KubernetesBuilder.test.ts | 8 +- .../src/service/KubernetesBuilder.ts | 11 ++- .../service/KubernetesFanOutHandler.test.ts | 85 +++++++++++------ .../src/service/KubernetesFetcher.test.ts | 90 +++++++++++------- .../src/service/KubernetesFetcher.ts | 6 +- .../src/service/KubernetesProxy.test.ts | 63 ++++++++----- .../src/service/KubernetesProxy.ts | 2 +- plugins/kubernetes-backend/src/types/types.ts | 12 ++- 33 files changed, 498 insertions(+), 309 deletions(-) diff --git a/plugins/kubernetes-backend/api-report.md b/plugins/kubernetes-backend/api-report.md index 17f1a4e5d9..aee7b16bae 100644 --- a/plugins/kubernetes-backend/api-report.md +++ b/plugins/kubernetes-backend/api-report.md @@ -28,6 +28,8 @@ export class AksStrategy implements AuthenticationStrategy { clusterDetails: ClusterDetails, auth: KubernetesRequestAuth, ): Promise; + // (undocumented) + validate(_: AuthMetadata): void; } // @public (undocumented) @@ -37,8 +39,13 @@ export interface AuthenticationStrategy { clusterDetails: ClusterDetails, authConfig: KubernetesRequestAuth, ): Promise; + // (undocumented) + validate(authMetadata: AuthMetadata): void; } +// @public +export type AuthMetadata = Record; + // @public (undocumented) export class AwsIamStrategy implements AuthenticationStrategy { constructor(opts: { config: Config }); @@ -46,6 +53,8 @@ export class AwsIamStrategy implements AuthenticationStrategy { decorateClusterDetailsWithAuth( clusterDetails: ClusterDetails, ): Promise; + // (undocumented) + validate(_: AuthMetadata): void; } // @public (undocumented) @@ -55,13 +64,14 @@ export class AzureIdentityStrategy implements AuthenticationStrategy { decorateClusterDetailsWithAuth( clusterDetails: ClusterDetails, ): Promise; + // (undocumented) + validate(_: AuthMetadata): void; } // @public (undocumented) export interface ClusterDetails { - authMetadata?: Record; // (undocumented) - authProvider: string; + authMetadata: AuthMetadata; // (undocumented) caData?: string | undefined; // (undocumented) @@ -104,6 +114,8 @@ export class DispatchStrategy implements AuthenticationStrategy { clusterDetails: ClusterDetails, auth: KubernetesRequestAuth, ): Promise; + // (undocumented) + validate(authMetadata: AuthMetadata): void; } // @public (undocumented) @@ -127,6 +139,8 @@ export class GoogleServiceAccountStrategy implements AuthenticationStrategy { decorateClusterDetailsWithAuth( clusterDetails: ClusterDetails, ): Promise; + // (undocumented) + validate(_: AuthMetadata): void; } // @public (undocumented) @@ -136,6 +150,8 @@ export class GoogleStrategy implements AuthenticationStrategy { clusterDetails: ClusterDetails, authConfig: KubernetesRequestAuth, ): Promise; + // (undocumented) + validate(_: AuthMetadata): void; } // @public @@ -375,6 +391,8 @@ export class NoopStrategy implements AuthenticationStrategy { decorateClusterDetailsWithAuth( clusterDetails: ClusterDetails, ): Promise; + // (undocumented) + validate(_: AuthMetadata): void; } // @public (undocumented) @@ -415,6 +433,8 @@ export class OidcStrategy implements AuthenticationStrategy { clusterDetails: ClusterDetails, authConfig: KubernetesRequestAuth, ): Promise; + // (undocumented) + validate(authMetadata: AuthMetadata): void; } // @public (undocumented) diff --git a/plugins/kubernetes-backend/src/auth/AksStrategy.test.ts b/plugins/kubernetes-backend/src/auth/AksStrategy.test.ts index 47d045d6b2..6974e302ee 100644 --- a/plugins/kubernetes-backend/src/auth/AksStrategy.test.ts +++ b/plugins/kubernetes-backend/src/auth/AksStrategy.test.ts @@ -20,10 +20,10 @@ describe('AksStrategy', () => { const strategy = new AksStrategy(); const details = await strategy.decorateClusterDetailsWithAuth( - { name: '', authProvider: 'aks', url: '' }, + { name: '', url: '', authMetadata: { authProvider: 'aks' } }, { aks: 'aksToken' }, ); - expect(details.authMetadata!.serviceAccountToken).toBe('aksToken'); + expect(details.authMetadata.serviceAccountToken).toBe('aksToken'); }); }); diff --git a/plugins/kubernetes-backend/src/auth/AksStrategy.ts b/plugins/kubernetes-backend/src/auth/AksStrategy.ts index 8bd22bcb64..0ed2967406 100644 --- a/plugins/kubernetes-backend/src/auth/AksStrategy.ts +++ b/plugins/kubernetes-backend/src/auth/AksStrategy.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { ClusterDetails } from '../types/types'; +import { AuthMetadata, ClusterDetails } from '../types/types'; import { AuthenticationStrategy } from './types'; import { KubernetesRequestAuth } from '@backstage/plugin-kubernetes-common'; @@ -22,7 +22,7 @@ import { KubernetesRequestAuth } from '@backstage/plugin-kubernetes-common'; * @public */ export class AksStrategy implements AuthenticationStrategy { - async decorateClusterDetailsWithAuth( + public async decorateClusterDetailsWithAuth( clusterDetails: ClusterDetails, auth: KubernetesRequestAuth, ): Promise { @@ -36,4 +36,5 @@ export class AksStrategy implements AuthenticationStrategy { }), }; } + public validate(_: AuthMetadata) {} } diff --git a/plugins/kubernetes-backend/src/auth/AwsIamStrategy.test.ts b/plugins/kubernetes-backend/src/auth/AwsIamStrategy.test.ts index b7182c6c96..0d5fb9a83f 100644 --- a/plugins/kubernetes-backend/src/auth/AwsIamStrategy.test.ts +++ b/plugins/kubernetes-backend/src/auth/AwsIamStrategy.test.ts @@ -63,9 +63,9 @@ describe('AwsIamStrategy tests', () => { const authPromise = strategy.decorateClusterDetailsWithAuth({ name: 'test-cluster', url: '', - authProvider: 'aws', + authMetadata: { authProvider: 'aws' }, }); - expect((await authPromise).authMetadata!.serviceAccountToken).toEqual( + expect((await authPromise).authMetadata.serviceAccountToken).toEqual( 'k8s-aws-v1.aHR0cHM6Ly9odHRwczovL2V4YW1wbGUuY29tL2FzZGY_', ); }); @@ -76,10 +76,12 @@ describe('AwsIamStrategy tests', () => { const authPromise = strategy.decorateClusterDetailsWithAuth({ name: 'test-cluster', url: '', - authProvider: 'aws', - authMetadata: { [ANNOTATION_KUBERNETES_AWS_ASSUME_ROLE]: 'SomeRole' }, + authMetadata: { + authProvider: 'aws', + [ANNOTATION_KUBERNETES_AWS_ASSUME_ROLE]: 'SomeRole', + }, }); - expect((await authPromise).authMetadata!.serviceAccountToken).toEqual( + expect((await authPromise).authMetadata.serviceAccountToken).toEqual( 'k8s-aws-v1.aHR0cHM6Ly9odHRwczovL2V4YW1wbGUuY29tL2FzZGY_', ); expect(fromTemporaryCredentials).toHaveBeenCalledWith({ @@ -102,13 +104,13 @@ describe('AwsIamStrategy tests', () => { const authPromise = strategy.decorateClusterDetailsWithAuth({ name: 'test-cluster', url: '', - authProvider: 'aws', authMetadata: { + authProvider: 'aws', [ANNOTATION_KUBERNETES_AWS_ASSUME_ROLE]: 'SomeRole', [ANNOTATION_KUBERNETES_AWS_EXTERNAL_ID]: 'external-id', }, }); - expect((await authPromise).authMetadata!.serviceAccountToken).toEqual( + expect((await authPromise).authMetadata.serviceAccountToken).toEqual( 'k8s-aws-v1.aHR0cHM6Ly9odHRwczovL2V4YW1wbGUuY29tL2FzZGY_', ); expect(fromTemporaryCredentials).toHaveBeenCalledWith({ @@ -137,7 +139,7 @@ describe('AwsIamStrategy tests', () => { strategy.decorateClusterDetailsWithAuth({ name: 'test-cluster', url: '', - authProvider: 'aws', + authMetadata: { authProvider: 'aws' }, }), ).rejects.toThrow('no way'); }); diff --git a/plugins/kubernetes-backend/src/auth/AwsIamStrategy.ts b/plugins/kubernetes-backend/src/auth/AwsIamStrategy.ts index 131535a67b..21a556002b 100644 --- a/plugins/kubernetes-backend/src/auth/AwsIamStrategy.ts +++ b/plugins/kubernetes-backend/src/auth/AwsIamStrategy.ts @@ -25,7 +25,7 @@ import { ANNOTATION_KUBERNETES_AWS_ASSUME_ROLE, ANNOTATION_KUBERNETES_AWS_EXTERNAL_ID, } from '@backstage/plugin-kubernetes-common'; -import { ClusterDetails } from '../types/types'; +import { AuthMetadata, ClusterDetails } from '../types/types'; import { AuthenticationStrategy } from './types'; /** @@ -46,10 +46,32 @@ const defaultRegion = 'us-east-1'; */ export class AwsIamStrategy implements AuthenticationStrategy { private readonly credsManager: AwsCredentialsManager; + constructor(opts: { config: Config }) { this.credsManager = DefaultAwsCredentialsManager.fromConfig(opts.config); } + public async decorateClusterDetailsWithAuth( + clusterDetails: ClusterDetails, + ): Promise { + const clusterDetailsWithAuthToken: ClusterDetails = Object.assign( + {}, + clusterDetails, + ); + + clusterDetailsWithAuthToken.authMetadata = { + serviceAccountToken: await this.getBearerToken( + clusterDetails.name, + clusterDetails.authMetadata[ANNOTATION_KUBERNETES_AWS_ASSUME_ROLE], + clusterDetails.authMetadata[ANNOTATION_KUBERNETES_AWS_EXTERNAL_ID], + ), + ...clusterDetailsWithAuthToken.authMetadata, + }; + return clusterDetailsWithAuthToken; + } + + public validate(_: AuthMetadata) {} + private async getBearerToken( clusterName: string, assumeRole?: string, @@ -110,23 +132,4 @@ export class AwsIamStrategy implements AuthenticationStrategy { return `k8s-aws-v1.${Buffer.from(url).toString('base64url')}`; } - - async decorateClusterDetailsWithAuth( - clusterDetails: ClusterDetails, - ): Promise { - const clusterDetailsWithAuthToken: ClusterDetails = Object.assign( - {}, - clusterDetails, - ); - - clusterDetailsWithAuthToken.authMetadata = { - serviceAccountToken: await this.getBearerToken( - clusterDetails.name, - clusterDetails.authMetadata?.[ANNOTATION_KUBERNETES_AWS_ASSUME_ROLE], - clusterDetails.authMetadata?.[ANNOTATION_KUBERNETES_AWS_EXTERNAL_ID], - ), - ...clusterDetailsWithAuthToken.authMetadata, - }; - return clusterDetailsWithAuthToken; - } } diff --git a/plugins/kubernetes-backend/src/auth/AzureIdentityStrategy.test.ts b/plugins/kubernetes-backend/src/auth/AzureIdentityStrategy.test.ts index 2325593271..e6aeb9dea1 100644 --- a/plugins/kubernetes-backend/src/auth/AzureIdentityStrategy.test.ts +++ b/plugins/kubernetes-backend/src/auth/AzureIdentityStrategy.test.ts @@ -41,9 +41,9 @@ class StaticTokenCredential implements TokenCredential { describe('AzureIdentityStrategy tests', () => { const cd = { - authProvider: 'Azure', name: 'My Cluster', url: 'mycluster.privatelink.westeurope.azmk8s.io', + authMetadata: { authProvider: 'azure' }, }; afterEach(() => { @@ -57,7 +57,7 @@ describe('AzureIdentityStrategy tests', () => { ); const response = await strategy.decorateClusterDetailsWithAuth(cd); - expect(response.authMetadata!.serviceAccountToken).toEqual('MY_TOKEN_1'); + expect(response.authMetadata.serviceAccountToken).toEqual('MY_TOKEN_1'); }); it('should re-use token before expiry', async () => { @@ -67,10 +67,10 @@ describe('AzureIdentityStrategy tests', () => { ); const response = await strategy.decorateClusterDetailsWithAuth(cd); - expect(response.authMetadata!.serviceAccountToken).toEqual('MY_TOKEN_1'); + expect(response.authMetadata.serviceAccountToken).toEqual('MY_TOKEN_1'); const response2 = await strategy.decorateClusterDetailsWithAuth(cd); - expect(response2.authMetadata!.serviceAccountToken).toEqual('MY_TOKEN_1'); + expect(response2.authMetadata.serviceAccountToken).toEqual('MY_TOKEN_1'); }); it('should issue new token 15 minutes befory expiry', async () => { @@ -82,12 +82,12 @@ describe('AzureIdentityStrategy tests', () => { ); const response = await strategy.decorateClusterDetailsWithAuth(cd); - expect(response.authMetadata!.serviceAccountToken).toEqual('MY_TOKEN_1'); + expect(response.authMetadata.serviceAccountToken).toEqual('MY_TOKEN_1'); jest.setSystemTime(Date.now() + 2 * 60 * 1000); // advance time by 2mins const response2 = await strategy.decorateClusterDetailsWithAuth(cd); - expect(response2.authMetadata!.serviceAccountToken).toEqual('MY_TOKEN_2'); + expect(response2.authMetadata.serviceAccountToken).toEqual('MY_TOKEN_2'); }); it('should re-use existing token if there is afailure', async () => { @@ -99,20 +99,20 @@ describe('AzureIdentityStrategy tests', () => { ); const response = await strategy.decorateClusterDetailsWithAuth(cd); - expect(response.authMetadata!.serviceAccountToken).toEqual('MY_TOKEN_1'); + expect(response.authMetadata.serviceAccountToken).toEqual('MY_TOKEN_1'); jest.setSystemTime(Date.now() + 2 * 60 * 1000); // advance time by 2min const response2 = await strategy.decorateClusterDetailsWithAuth(cd); - expect(response2.authMetadata!.serviceAccountToken).toEqual('MY_TOKEN_2'); + expect(response2.authMetadata.serviceAccountToken).toEqual('MY_TOKEN_2'); jest.setSystemTime(Date.now() + 2 * 60 * 1000); // advance time by 2min const response3 = await strategy.decorateClusterDetailsWithAuth(cd); - expect(response3.authMetadata!.serviceAccountToken).toEqual('MY_TOKEN_2'); + expect(response3.authMetadata.serviceAccountToken).toEqual('MY_TOKEN_2'); const response4 = await strategy.decorateClusterDetailsWithAuth(cd); - expect(response4.authMetadata!.serviceAccountToken).toEqual('MY_TOKEN_4'); + expect(response4.authMetadata.serviceAccountToken).toEqual('MY_TOKEN_4'); }); it('should throw if existing token expired and failed to fetch a new one', async () => { @@ -124,12 +124,12 @@ describe('AzureIdentityStrategy tests', () => { ); const response = await strategy.decorateClusterDetailsWithAuth(cd); - expect(response.authMetadata!.serviceAccountToken).toEqual('MY_TOKEN_1'); + expect(response.authMetadata.serviceAccountToken).toEqual('MY_TOKEN_1'); jest.setSystemTime(Date.now() + 2 * 60 * 1000); // advance time by 2min const response2 = await strategy.decorateClusterDetailsWithAuth(cd); - expect(response2.authMetadata!.serviceAccountToken).toEqual('MY_TOKEN_2'); + expect(response2.authMetadata.serviceAccountToken).toEqual('MY_TOKEN_2'); jest.setSystemTime(Date.now() + 17 * 60 * 1000); // advance time by 17min diff --git a/plugins/kubernetes-backend/src/auth/AzureIdentityStrategy.ts b/plugins/kubernetes-backend/src/auth/AzureIdentityStrategy.ts index 038100a19f..f04af9d37e 100644 --- a/plugins/kubernetes-backend/src/auth/AzureIdentityStrategy.ts +++ b/plugins/kubernetes-backend/src/auth/AzureIdentityStrategy.ts @@ -16,7 +16,7 @@ import { Logger } from 'winston'; import { AuthenticationStrategy } from './types'; -import { ClusterDetails } from '../types/types'; +import { AuthMetadata, ClusterDetails } from '../types/types'; import { AccessToken, DefaultAzureCredential, @@ -38,7 +38,7 @@ export class AzureIdentityStrategy implements AuthenticationStrategy { private readonly tokenCredential: TokenCredential = new DefaultAzureCredential(), ) {} - async decorateClusterDetailsWithAuth( + public async decorateClusterDetailsWithAuth( clusterDetails: ClusterDetails, ): Promise { const clusterDetailsWithAuthToken: ClusterDetails = Object.assign( @@ -53,6 +53,8 @@ export class AzureIdentityStrategy implements AuthenticationStrategy { return clusterDetailsWithAuthToken; } + public validate(_: AuthMetadata) {} + private async getToken(): Promise { if (!this.tokenRequiresRefresh()) { return this.accessToken.token; diff --git a/plugins/kubernetes-backend/src/auth/DispatchStrategy.test.ts b/plugins/kubernetes-backend/src/auth/DispatchStrategy.test.ts index d53f9f1e6c..07adff946a 100644 --- a/plugins/kubernetes-backend/src/auth/DispatchStrategy.test.ts +++ b/plugins/kubernetes-backend/src/auth/DispatchStrategy.test.ts @@ -25,7 +25,10 @@ describe('decorateClusterDetailsWithAuth', () => { const authObject: KubernetesRequestAuth = {}; beforeEach(() => { - mockStrategy = { decorateClusterDetailsWithAuth: jest.fn() }; + mockStrategy = { + decorateClusterDetailsWithAuth: jest.fn(), + validate: jest.fn(), + }; strategy = new DispatchStrategy({ authStrategyMap: { google: mockStrategy }, }); @@ -35,8 +38,10 @@ describe('decorateClusterDetailsWithAuth', () => { const expectedClusterDetails: ClusterDetails = { url: 'notanything.com', name: 'randomName', - authProvider: 'google', - authMetadata: { serviceAccountToken: 'added by mock strategy' }, + authMetadata: { + authProvider: 'google', + serviceAccountToken: 'added by mock strategy', + }, }; mockStrategy.decorateClusterDetailsWithAuth.mockResolvedValue( @@ -44,12 +49,20 @@ describe('decorateClusterDetailsWithAuth', () => { ); const returnedValue = await strategy.decorateClusterDetailsWithAuth( - { name: 'googleCluster', url: 'anything.com', authProvider: 'google' }, + { + name: 'googleCluster', + url: 'anything.com', + authMetadata: { authProvider: 'google' }, + }, authObject, ); expect(mockStrategy.decorateClusterDetailsWithAuth).toHaveBeenCalledWith( - { name: 'googleCluster', url: 'anything.com', authProvider: 'google' }, + { + name: 'googleCluster', + url: 'anything.com', + authMetadata: { authProvider: 'google' }, + }, authObject, ); expect(returnedValue).toBe(expectedClusterDetails); @@ -61,7 +74,7 @@ describe('decorateClusterDetailsWithAuth', () => { { name: 'test-cluster', url: 'anything.com', - authProvider: 'linode', + authMetadata: { authProvider: 'linode' }, }, authObject, ), diff --git a/plugins/kubernetes-backend/src/auth/DispatchStrategy.ts b/plugins/kubernetes-backend/src/auth/DispatchStrategy.ts index c40ff66d27..6791bdbe3e 100644 --- a/plugins/kubernetes-backend/src/auth/DispatchStrategy.ts +++ b/plugins/kubernetes-backend/src/auth/DispatchStrategy.ts @@ -15,7 +15,7 @@ */ import { AuthenticationStrategy } from './types'; -import { ClusterDetails } from '../types'; +import { AuthMetadata, ClusterDetails } from '../types'; import { KubernetesRequestAuth } from '@backstage/plugin-kubernetes-common'; /** @@ -42,13 +42,25 @@ export class DispatchStrategy implements AuthenticationStrategy { clusterDetails: ClusterDetails, auth: KubernetesRequestAuth, ) { - if (this.strategyMap[clusterDetails.authProvider]) { - return this.strategyMap[ - clusterDetails.authProvider - ].decorateClusterDetailsWithAuth(clusterDetails, auth); + const authProvider = clusterDetails.authMetadata.authProvider; + if (this.strategyMap[authProvider]) { + return this.strategyMap[authProvider].decorateClusterDetailsWithAuth( + clusterDetails, + auth, + ); } throw new Error( - `authProvider "${clusterDetails.authProvider}" has no AuthenticationStrategy associated with it`, + `authProvider "${authProvider}" has no AuthenticationStrategy associated with it`, ); } + + public validate(authMetadata: AuthMetadata) { + const strategy = this.strategyMap[authMetadata.authProvider]; + if (!strategy) { + throw new Error( + `authProvider "${authMetadata.authProvider}" has no config associated with it`, + ); + } + strategy.validate(authMetadata); + } } diff --git a/plugins/kubernetes-backend/src/auth/GoogleServiceAccountStrategy.ts b/plugins/kubernetes-backend/src/auth/GoogleServiceAccountStrategy.ts index db8d9d2c2c..dcb230eaf7 100644 --- a/plugins/kubernetes-backend/src/auth/GoogleServiceAccountStrategy.ts +++ b/plugins/kubernetes-backend/src/auth/GoogleServiceAccountStrategy.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import { AuthenticationStrategy } from './types'; -import { ClusterDetails } from '../types/types'; +import { AuthMetadata, ClusterDetails } from '../types/types'; import * as container from '@google-cloud/container'; /** @@ -22,7 +22,7 @@ import * as container from '@google-cloud/container'; * @public */ export class GoogleServiceAccountStrategy implements AuthenticationStrategy { - async decorateClusterDetailsWithAuth( + public async decorateClusterDetailsWithAuth( clusterDetails: ClusterDetails, ): Promise { const clusterDetailsWithAuthToken: ClusterDetails = Object.assign( @@ -44,4 +44,6 @@ export class GoogleServiceAccountStrategy implements AuthenticationStrategy { } return clusterDetailsWithAuthToken; } + + public validate(_: AuthMetadata) {} } diff --git a/plugins/kubernetes-backend/src/auth/GoogleStrategy.ts b/plugins/kubernetes-backend/src/auth/GoogleStrategy.ts index c14f85051c..79d9df2059 100644 --- a/plugins/kubernetes-backend/src/auth/GoogleStrategy.ts +++ b/plugins/kubernetes-backend/src/auth/GoogleStrategy.ts @@ -15,7 +15,7 @@ */ import { AuthenticationStrategy } from './types'; -import { ClusterDetails } from '../types/types'; +import { AuthMetadata, ClusterDetails } from '../types/types'; import { KubernetesRequestAuth } from '@backstage/plugin-kubernetes-common'; /** @@ -23,7 +23,7 @@ import { KubernetesRequestAuth } from '@backstage/plugin-kubernetes-common'; * @public */ export class GoogleStrategy implements AuthenticationStrategy { - async decorateClusterDetailsWithAuth( + public async decorateClusterDetailsWithAuth( clusterDetails: ClusterDetails, authConfig: KubernetesRequestAuth, ): Promise { @@ -45,4 +45,5 @@ export class GoogleStrategy implements AuthenticationStrategy { } return clusterDetailsWithAuthToken; } + public validate(_: AuthMetadata) {} } diff --git a/plugins/kubernetes-backend/src/auth/NoopStrategy.ts b/plugins/kubernetes-backend/src/auth/NoopStrategy.ts index c96d2a55c7..80841e2a19 100644 --- a/plugins/kubernetes-backend/src/auth/NoopStrategy.ts +++ b/plugins/kubernetes-backend/src/auth/NoopStrategy.ts @@ -15,16 +15,18 @@ */ import { AuthenticationStrategy } from './types'; -import { ClusterDetails } from '../types/types'; +import { AuthMetadata, ClusterDetails } from '../types/types'; /** * * @public */ export class NoopStrategy implements AuthenticationStrategy { - async decorateClusterDetailsWithAuth( + public async decorateClusterDetailsWithAuth( clusterDetails: ClusterDetails, ): Promise { return clusterDetails; } + + public validate(_: AuthMetadata) {} } diff --git a/plugins/kubernetes-backend/src/auth/OidcStrategy.test.ts b/plugins/kubernetes-backend/src/auth/OidcStrategy.test.ts index 1160308521..5650e0eccd 100644 --- a/plugins/kubernetes-backend/src/auth/OidcStrategy.test.ts +++ b/plugins/kubernetes-backend/src/auth/OidcStrategy.test.ts @@ -16,33 +16,38 @@ import { ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER } from '@backstage/plugin-kubernetes-common'; import { OidcStrategy } from './OidcStrategy'; -import { ClusterDetails } from '../types/types'; describe('OidcStrategy tests', () => { const strategy = new OidcStrategy(); - const baseClusterDetails: ClusterDetails = { - name: 'test', - authProvider: 'oidc', - url: '', - }; it('returns cluster details with auth token', async () => { const details = await strategy.decorateClusterDetailsWithAuth( { - authMetadata: { [ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER]: 'okta' }, - ...baseClusterDetails, + name: 'test', + url: '', + authMetadata: { + authProvider: 'oidc', + [ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER]: 'okta', + }, }, { oidc: { okta: 'fakeToken' }, }, ); - expect(details.authMetadata!.serviceAccountToken).toBe('fakeToken'); + expect(details.authMetadata.serviceAccountToken).toBe('fakeToken'); }); it('returns error when oidcTokenProvider is not configured', async () => { await expect( - strategy.decorateClusterDetailsWithAuth(baseClusterDetails, {}), + strategy.decorateClusterDetailsWithAuth( + { + name: 'test', + url: '', + authMetadata: { authProvider: 'oidc' }, + }, + {}, + ), ).rejects.toThrow( 'oidc authProvider requires a configured oidcTokenProvider', ); @@ -52,8 +57,12 @@ describe('OidcStrategy tests', () => { await expect( strategy.decorateClusterDetailsWithAuth( { - authMetadata: { [ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER]: 'okta' }, - ...baseClusterDetails, + name: 'test', + url: '', + authMetadata: { + authProvider: 'oidc', + [ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER]: 'okta', + }, }, {}, ), diff --git a/plugins/kubernetes-backend/src/auth/OidcStrategy.ts b/plugins/kubernetes-backend/src/auth/OidcStrategy.ts index db41ba6776..083c28dd1b 100644 --- a/plugins/kubernetes-backend/src/auth/OidcStrategy.ts +++ b/plugins/kubernetes-backend/src/auth/OidcStrategy.ts @@ -18,14 +18,14 @@ import { KubernetesRequestAuth, } from '@backstage/plugin-kubernetes-common'; import { AuthenticationStrategy } from './types'; -import { ClusterDetails } from '../types/types'; +import { AuthMetadata, ClusterDetails } from '../types/types'; /** * * @public */ export class OidcStrategy implements AuthenticationStrategy { - async decorateClusterDetailsWithAuth( + public async decorateClusterDetailsWithAuth( clusterDetails: ClusterDetails, authConfig: KubernetesRequestAuth, ): Promise { @@ -35,7 +35,7 @@ export class OidcStrategy implements AuthenticationStrategy { ); const oidcTokenProvider = - clusterDetails.authMetadata?.[ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER]; + clusterDetails.authMetadata[ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER]; if (!oidcTokenProvider || oidcTokenProvider === '') { throw new Error( @@ -57,4 +57,12 @@ export class OidcStrategy implements AuthenticationStrategy { } return clusterDetailsWithAuthToken; } + + public validate(authMetadata: AuthMetadata) { + const oidcTokenProvider = + authMetadata[ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER]; + if (!oidcTokenProvider || oidcTokenProvider === '') { + throw new Error(`Must specify a token provider for 'oidc' strategy`); + } + } } diff --git a/plugins/kubernetes-backend/src/auth/types.ts b/plugins/kubernetes-backend/src/auth/types.ts index 79a581bfd0..ed57ecc1f6 100644 --- a/plugins/kubernetes-backend/src/auth/types.ts +++ b/plugins/kubernetes-backend/src/auth/types.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { ClusterDetails } from '../types/types'; +import { AuthMetadata, ClusterDetails } from '../types/types'; import { KubernetesRequestAuth } from '@backstage/plugin-kubernetes-common'; /** @@ -26,4 +26,5 @@ export interface AuthenticationStrategy { clusterDetails: ClusterDetails, authConfig: KubernetesRequestAuth, ): Promise; + validate(authMetadata: AuthMetadata): void; } diff --git a/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.test.ts b/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.test.ts index 2f6d3297e1..42424f4a0d 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.test.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.test.ts @@ -96,8 +96,8 @@ describe('CatalogClusterLocator', () => { name: 'owned', url: 'https://apiserver.com', caData: 'caData', - authProvider: 'oidc', authMetadata: { + authProvider: 'oidc', 'kubernetes.io/api-server': 'https://apiserver.com', 'kubernetes.io/api-server-certificate-authority': 'caData', 'kubernetes.io/auth-provider': 'oidc', @@ -124,8 +124,8 @@ describe('CatalogClusterLocator', () => { name: 'owned', url: 'https://apiserver.com', caData: 'caData', - authProvider: 'aws', authMetadata: { + authProvider: 'aws', 'kubernetes.io/api-server': 'https://apiserver.com', 'kubernetes.io/api-server-certificate-authority': 'caData', 'kubernetes.io/auth-provider': 'aws', diff --git a/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.ts b/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.ts index a0088d4b79..b2499639b0 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.ts @@ -59,9 +59,15 @@ export class CatalogClusterLocator implements KubernetesClustersSupplier { url: entity.metadata.annotations![ANNOTATION_KUBERNETES_API_SERVER]!, caData: entity.metadata.annotations![ANNOTATION_KUBERNETES_API_SERVER_CA]!, - authProvider: - entity.metadata.annotations![ANNOTATION_KUBERNETES_AUTH_PROVIDER]!, - ...{ authMetadata: entity.metadata.annotations }, + ...{ + authMetadata: { + authProvider: + entity.metadata.annotations![ + ANNOTATION_KUBERNETES_AUTH_PROVIDER + ]!, + ...entity.metadata.annotations, + }, + }, skipMetricsLookup: entity.metadata.annotations![ ANNOTATION_KUBERNETES_SKIP_METRICS_LOOKUP diff --git a/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.test.ts b/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.test.ts index e1229aecf3..58e21909f7 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.test.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.test.ts @@ -55,7 +55,7 @@ describe('ConfigClusterLocator', () => { { name: 'cluster1', url: 'http://localhost:8080', - authProvider: 'serviceAccount', + authMetadata: { authProvider: 'serviceAccount' }, skipMetricsLookup: false, skipTLSVerify: false, caData: undefined, @@ -95,17 +95,19 @@ describe('ConfigClusterLocator', () => { name: 'cluster1', dashboardUrl: 'https://k8s.foo.com', url: 'http://localhost:8080', - authProvider: 'serviceAccount', + authMetadata: { + authProvider: 'serviceAccount', + serviceAccountToken: 'token', + }, skipTLSVerify: false, skipMetricsLookup: true, caData: undefined, caFile: undefined, - authMetadata: { serviceAccountToken: 'token' }, }, { name: 'cluster2', url: 'http://localhost:8081', - authProvider: 'google', + authMetadata: { authProvider: 'google' }, skipTLSVerify: true, skipMetricsLookup: false, caData: undefined, @@ -150,8 +152,8 @@ describe('ConfigClusterLocator', () => { { name: 'cluster1', url: 'http://localhost:8080', - authProvider: 'aws', authMetadata: { + authProvider: 'aws', serviceAccountToken: 'token', }, skipTLSVerify: false, @@ -162,8 +164,8 @@ describe('ConfigClusterLocator', () => { { name: 'cluster2', url: 'http://localhost:8081', - authProvider: 'aws', authMetadata: { + authProvider: 'aws', [ANNOTATION_KUBERNETES_AWS_ASSUME_ROLE]: 'SomeRole', }, skipTLSVerify: true, @@ -174,8 +176,8 @@ describe('ConfigClusterLocator', () => { { name: 'cluster2', url: 'http://localhost:8081', - authProvider: 'aws', authMetadata: { + authProvider: 'aws', [ANNOTATION_KUBERNETES_AWS_ASSUME_ROLE]: 'SomeRole', [ANNOTATION_KUBERNETES_AWS_EXTERNAL_ID]: 'SomeExternalId', }, @@ -212,7 +214,7 @@ describe('ConfigClusterLocator', () => { { name: 'cluster1', url: 'http://localhost:8080', - authProvider: 'serviceAccount', + authMetadata: { authProvider: 'serviceAccount' }, skipMetricsLookup: false, skipTLSVerify: false, caData: undefined, @@ -248,7 +250,7 @@ describe('ConfigClusterLocator', () => { { name: 'cluster1', url: 'http://localhost:8080', - authProvider: 'serviceAccount', + authMetadata: { authProvider: 'serviceAccount' }, skipMetricsLookup: false, skipTLSVerify: false, caData: undefined, @@ -260,18 +262,31 @@ describe('ConfigClusterLocator', () => { }); it('supports aks authProvider', async () => { - const cluster = { - name: 'aks-cluster', - url: 'https://aks.test', - authProvider: 'aks', - }; const sut = ConfigClusterLocator.fromConfig( - new ConfigReader({ clusters: [cluster] }), + new ConfigReader({ + clusters: [ + { + name: 'aks-cluster', + url: 'https://aks.test', + authProvider: 'aks', + }, + ], + }), ); const result = await sut.getClusters(); - expect(result).toMatchObject([cluster]); + expect(result).toStrictEqual([ + { + name: 'aks-cluster', + url: 'https://aks.test', + authMetadata: { authProvider: 'aks' }, + skipMetricsLookup: false, + skipTLSVerify: false, + caData: undefined, + caFile: undefined, + }, + ]); }); it('has cluster level defined customResources returns clusterDetails with those CRDs', async () => { @@ -300,7 +315,7 @@ describe('ConfigClusterLocator', () => { { name: 'cluster1', url: 'http://localhost:8080', - authProvider: 'serviceAccount', + authMetadata: { authProvider: 'serviceAccount' }, skipMetricsLookup: false, skipTLSVerify: false, caData: undefined, @@ -316,18 +331,22 @@ describe('ConfigClusterLocator', () => { ]); }); + // TODO move this to a test on OidcStrategy#validate it('errors when authProvider is oidc but oidcTokenProvider is missing', async () => { - const cluster = { - name: 'oidc-cluster', - url: 'https://aks.test', - authProvider: 'oidc', - }; expect(() => ConfigClusterLocator.fromConfig( - new ConfigReader({ clusters: [cluster] }), + new ConfigReader({ + clusters: [ + { + name: 'oidc-cluster', + url: 'https://aks.test', + authProvider: 'oidc', + }, + ], + }), ), ).toThrow( - `Cluster 'oidc-cluster' missing required config value for 'oidcTokenProvider'`, + `Invalid cluster 'oidc-cluster': Must specify a token provider for 'oidc' strategy`, ); }); }); diff --git a/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.ts b/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.ts index acbb5f96fd..bba9a69635 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.ts @@ -21,38 +21,39 @@ import { ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER, } from '@backstage/plugin-kubernetes-common'; import { ClusterDetails, KubernetesClustersSupplier } from '../types/types'; +import { + AuthenticationStrategy, + AksStrategy, + DispatchStrategy, + GoogleStrategy, + GoogleServiceAccountStrategy, + NoopStrategy, + OidcStrategy, +} from '../auth'; + +const defaultAuthStrategies: Record = { + aks: new AksStrategy(), + aws: new NoopStrategy(), + google: new GoogleStrategy(), + googleServiceAccount: new GoogleServiceAccountStrategy(), + localKubectlProxy: new NoopStrategy(), + oidc: new OidcStrategy(), + serviceAccount: new NoopStrategy(), +}; export class ConfigClusterLocator implements KubernetesClustersSupplier { private readonly clusterDetails: ClusterDetails[]; - private static authValidators: Record< - string, - (clusterDetails: ClusterDetails) => void - > = { - google: _ => {}, - aws: _ => {}, - azure: _ => {}, - oidc: clusterDetails => { - if ( - !clusterDetails.authMetadata?.[ - ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER - ] - ) { - throw new Error( - `Cluster '${clusterDetails.name}' missing required config value for 'oidcTokenProvider'`, - ); - } - }, - serviceAccount: _ => {}, - googleServiceAccount: _ => {}, - aks: _ => {}, - }; - constructor(clusterDetails: ClusterDetails[]) { this.clusterDetails = clusterDetails; } - static fromConfig(config: Config): ConfigClusterLocator { + static fromConfig( + config: Config, + authStrategy: AuthenticationStrategy = new DispatchStrategy({ + authStrategyMap: defaultAuthStrategies, + }), + ): ConfigClusterLocator { return new ConfigClusterLocator( config.getConfigArray('clusters').map(c => { const authProvider = c.getString('authProvider'); @@ -63,8 +64,10 @@ export class ConfigClusterLocator implements KubernetesClustersSupplier { skipMetricsLookup: c.getOptionalBoolean('skipMetricsLookup') ?? false, caData: c.getOptionalString('caData'), caFile: c.getOptionalString('caFile'), - authProvider: authProvider, - ...ConfigClusterLocator.parseAuthMetadata(c), + authMetadata: { + authProvider, + ...ConfigClusterLocator.parseAuthMetadata(c), + }, }; const customResources = c.getOptionalConfigArray('customResources'); @@ -90,20 +93,21 @@ export class ConfigClusterLocator implements KubernetesClustersSupplier { clusterDetails.dashboardParameters = c.get('dashboardParameters'); } - if (authProvider in ConfigClusterLocator.authValidators) { - ConfigClusterLocator.authValidators[authProvider](clusterDetails); - return clusterDetails; + try { + authStrategy.validate(clusterDetails.authMetadata); + } catch (e) { + throw new Error( + `Invalid cluster '${clusterDetails.name}': ${e.message}`, + ); } - throw new Error( - `authProvider "${authProvider}" has no config associated with it`, - ); + return clusterDetails; }), ); } private static parseAuthMetadata( clusterConfig: Config, - ): { authMetadata: Record } | undefined { + ): Record | undefined { const serviceAccountToken = clusterConfig.getOptionalString( 'serviceAccountToken', ); @@ -114,18 +118,16 @@ export class ConfigClusterLocator implements KubernetesClustersSupplier { return serviceAccountToken || assumeRole || externalId || oidcTokenProvider ? { - authMetadata: { - ...(serviceAccountToken && { serviceAccountToken }), - ...(assumeRole && { - [ANNOTATION_KUBERNETES_AWS_ASSUME_ROLE]: assumeRole, - }), - ...(externalId && { - [ANNOTATION_KUBERNETES_AWS_EXTERNAL_ID]: externalId, - }), - ...(oidcTokenProvider && { - [ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER]: oidcTokenProvider, - }), - }, + ...(serviceAccountToken && { serviceAccountToken }), + ...(assumeRole && { + [ANNOTATION_KUBERNETES_AWS_ASSUME_ROLE]: assumeRole, + }), + ...(externalId && { + [ANNOTATION_KUBERNETES_AWS_EXTERNAL_ID]: externalId, + }), + ...(oidcTokenProvider && { + [ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER]: oidcTokenProvider, + }), } : undefined; } diff --git a/plugins/kubernetes-backend/src/cluster-locator/GkeClusterLocator.test.ts b/plugins/kubernetes-backend/src/cluster-locator/GkeClusterLocator.test.ts index 11dc4572ea..fef1e8a3ca 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/GkeClusterLocator.test.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/GkeClusterLocator.test.ts @@ -104,9 +104,9 @@ describe('GkeClusterLocator', () => { expect(result).toStrictEqual([ { - authProvider: 'google', name: 'some-cluster', url: 'https://1.2.3.4', + authMetadata: { authProvider: 'google' }, skipTLSVerify: false, skipMetricsLookup: true, }, @@ -141,9 +141,9 @@ describe('GkeClusterLocator', () => { expect(result).toStrictEqual([ { - authProvider: 'google', name: 'some-cluster', url: 'https://1.2.3.4', + authMetadata: { authProvider: 'google' }, skipTLSVerify: false, skipMetricsLookup: false, }, @@ -183,16 +183,16 @@ describe('GkeClusterLocator', () => { expect(result).toStrictEqual([ { - authProvider: 'google', name: 'some-cluster', url: 'https://1.2.3.4', + authMetadata: { authProvider: 'google' }, skipTLSVerify: false, skipMetricsLookup: false, }, { - authProvider: 'google', name: 'some-other-cluster', url: 'https://6.7.8.9', + authMetadata: { authProvider: 'google' }, skipTLSVerify: false, skipMetricsLookup: false, }, @@ -238,16 +238,16 @@ describe('GkeClusterLocator', () => { expect(result).toStrictEqual([ { - authProvider: 'google', name: 'some-cluster', url: 'https://1.2.3.4', + authMetadata: { authProvider: 'google' }, skipTLSVerify: false, skipMetricsLookup: false, }, { - authProvider: 'google', name: 'some-other-cluster', url: 'https://6.7.8.9', + authMetadata: { authProvider: 'google' }, skipTLSVerify: false, skipMetricsLookup: false, }, @@ -299,9 +299,9 @@ describe('GkeClusterLocator', () => { expect(result).toStrictEqual([ { - authProvider: 'google', name: 'some-cluster', url: 'https://1.2.3.4', + authMetadata: { authProvider: 'google' }, skipTLSVerify: false, skipMetricsLookup: false, }, @@ -363,9 +363,9 @@ describe('GkeClusterLocator', () => { expect(result).toStrictEqual([ { - authProvider: 'google', name: 'some-cluster', url: 'https://1.2.3.4', + authMetadata: { authProvider: 'google' }, skipTLSVerify: false, skipMetricsLookup: true, dashboardApp: 'gke', diff --git a/plugins/kubernetes-backend/src/cluster-locator/GkeClusterLocator.ts b/plugins/kubernetes-backend/src/cluster-locator/GkeClusterLocator.ts index 40a003607f..0e45898639 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/GkeClusterLocator.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/GkeClusterLocator.ts @@ -120,7 +120,7 @@ export class GkeClusterLocator implements KubernetesClustersSupplier { // TODO filter out clusters which don't have name or endpoint name: r.name ?? 'unknown', url: `https://${r.endpoint ?? ''}`, - authProvider: 'google', + authMetadata: { authProvider: 'google' }, skipTLSVerify, skipMetricsLookup, ...(exposeDashboard diff --git a/plugins/kubernetes-backend/src/cluster-locator/LocalKubectlProxyLocator.ts b/plugins/kubernetes-backend/src/cluster-locator/LocalKubectlProxyLocator.ts index b2c11fc744..d393087da2 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/LocalKubectlProxyLocator.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/LocalKubectlProxyLocator.ts @@ -26,7 +26,7 @@ export class LocalKubectlProxyClusterLocator { name: 'local', url: 'http:/localhost:8001', - authProvider: 'localKubectlProxy', + authMetadata: { authProvider: 'localKubectlProxy' }, skipMetricsLookup: true, }, ]; diff --git a/plugins/kubernetes-backend/src/cluster-locator/index.test.ts b/plugins/kubernetes-backend/src/cluster-locator/index.test.ts index e1bca888b5..9affb4c588 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/index.test.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/index.test.ts @@ -18,6 +18,7 @@ import { Config, ConfigReader } from '@backstage/config'; import { getCombinedClusterSupplier } from './index'; import { CatalogApi } from '@backstage/catalog-client'; import { ClusterDetails } from '../types/types'; +import { AuthenticationStrategy, DispatchStrategy } from '../auth'; describe('getCombinedClusterSupplier', () => { let catalogApi: CatalogApi; @@ -48,16 +49,31 @@ describe('getCombinedClusterSupplier', () => { }, 'ctx', ); + const mockStrategy: jest.Mocked = { + decorateClusterDetailsWithAuth: jest.fn(), + validate: jest.fn(), + }; - const clusterSupplier = getCombinedClusterSupplier(config, catalogApi); + const clusterSupplier = getCombinedClusterSupplier( + config, + catalogApi, + new DispatchStrategy({ + authStrategyMap: { + serviceAccount: mockStrategy, + google: mockStrategy, + }, + }), + ); const result = await clusterSupplier.getClusters(); expect(result).toStrictEqual([ { name: 'cluster1', url: 'http://localhost:8080', - authProvider: 'serviceAccount', - authMetadata: { serviceAccountToken: 'token' }, + authMetadata: { + authProvider: 'serviceAccount', + serviceAccountToken: 'token', + }, skipMetricsLookup: false, skipTLSVerify: false, caData: undefined, @@ -66,7 +82,7 @@ describe('getCombinedClusterSupplier', () => { { name: 'cluster2', url: 'http://localhost:8081', - authProvider: 'google', + authMetadata: { authProvider: 'google' }, skipMetricsLookup: false, skipTLSVerify: false, caData: undefined, @@ -77,35 +93,17 @@ describe('getCombinedClusterSupplier', () => { it('throws an error when using an unsupported cluster locator', async () => { const config: Config = new ConfigReader( - { - kubernetes: { - clusterLocatorMethods: [ - { - type: 'config', - clusters: [ - { - name: 'cluster1', - serviceAccountToken: 'token', - url: 'http://localhost:8080', - authProvider: 'serviceAccount', - }, - { - name: 'cluster2', - url: 'http://localhost:8081', - authProvider: 'google', - }, - ], - }, - { - type: 'magic', - }, - ], - }, - }, + { kubernetes: { clusterLocatorMethods: [{ type: 'magic' }] } }, 'ctx', ); - expect(() => getCombinedClusterSupplier(config, catalogApi)).toThrow( + expect(() => + getCombinedClusterSupplier( + config, + catalogApi, + new DispatchStrategy({ authStrategyMap: {} }), + ), + ).toThrow( new Error('Unsupported kubernetes.clusterLocatorMethods: "magic"'), ); }); diff --git a/plugins/kubernetes-backend/src/cluster-locator/index.ts b/plugins/kubernetes-backend/src/cluster-locator/index.ts index 62dfe15720..03a459b634 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/index.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/index.ts @@ -17,6 +17,7 @@ import { Config } from '@backstage/config'; import { Duration } from 'luxon'; import { ClusterDetails, KubernetesClustersSupplier } from '../types/types'; +import { AuthenticationStrategy } from '../auth/types'; import { ConfigClusterLocator } from './ConfigClusterLocator'; import { GkeClusterLocator } from './GkeClusterLocator'; import { CatalogClusterLocator } from './CatalogClusterLocator'; @@ -42,6 +43,7 @@ class CombinedClustersSupplier implements KubernetesClustersSupplier { export const getCombinedClusterSupplier = ( rootConfig: Config, catalogClient: CatalogApi, + authStrategy: AuthenticationStrategy, refreshInterval: Duration | undefined = undefined, ): KubernetesClustersSupplier => { const clusterSuppliers = rootConfig @@ -54,7 +56,10 @@ export const getCombinedClusterSupplier = ( case 'localKubectlProxy': return new LocalKubectlProxyClusterLocator(); case 'config': - return ConfigClusterLocator.fromConfig(clusterLocatorMethod); + return ConfigClusterLocator.fromConfig( + clusterLocatorMethod, + authStrategy, + ); case 'gke': return GkeClusterLocator.fromConfig( clusterLocatorMethod, diff --git a/plugins/kubernetes-backend/src/service-locator/MultiTenantServiceLocator.test.ts b/plugins/kubernetes-backend/src/service-locator/MultiTenantServiceLocator.test.ts index b40503efc9..18cc72ffb1 100644 --- a/plugins/kubernetes-backend/src/service-locator/MultiTenantServiceLocator.test.ts +++ b/plugins/kubernetes-backend/src/service-locator/MultiTenantServiceLocator.test.ts @@ -16,7 +16,7 @@ import '@backstage/backend-common'; import { Entity } from '@backstage/catalog-model'; -import { ServiceLocatorRequestContext } from '../types/types'; +import { AuthMetadata, ServiceLocatorRequestContext } from '../types/types'; import { MultiTenantServiceLocator } from './MultiTenantServiceLocator'; describe('MultiTenantConfigClusterLocator', () => { @@ -40,8 +40,10 @@ describe('MultiTenantConfigClusterLocator', () => { { name: 'cluster1', url: 'http://localhost:8080', - authProvider: 'serviceAccount', - serviceAccountToken: '12345', + authMetadata: { + authProvider: 'serviceAccount', + serviceAccountToken: '12345', + }, }, ]; }, @@ -56,9 +58,11 @@ describe('MultiTenantConfigClusterLocator', () => { clusters: [ { name: 'cluster1', - serviceAccountToken: '12345', url: 'http://localhost:8080', - authProvider: 'serviceAccount', + authMetadata: { + authProvider: 'serviceAccount', + serviceAccountToken: '12345', + }, }, ], }); @@ -70,14 +74,16 @@ describe('MultiTenantConfigClusterLocator', () => { return [ { name: 'cluster1', - serviceAccountToken: 'token', url: 'http://localhost:8080', - authProvider: 'serviceAccount', + authMetadata: { + authProvider: 'serviceAccount', + serviceAccountToken: 'token', + } as AuthMetadata, }, { name: 'cluster2', url: 'http://localhost:8081', - authProvider: 'google', + authMetadata: { authProvider: 'google' } as AuthMetadata, }, ]; }, @@ -92,14 +98,16 @@ describe('MultiTenantConfigClusterLocator', () => { clusters: [ { name: 'cluster1', - serviceAccountToken: 'token', url: 'http://localhost:8080', - authProvider: 'serviceAccount', + authMetadata: { + authProvider: 'serviceAccount', + serviceAccountToken: 'token', + }, }, { name: 'cluster2', url: 'http://localhost:8081', - authProvider: 'google', + authMetadata: { authProvider: 'google' }, }, ], }); diff --git a/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts b/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts index 9d663a4185..7c00c1b162 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts @@ -62,13 +62,13 @@ describe('KubernetesBuilder', () => { const clusters: ClusterDetails[] = [ { name: 'some-cluster', - authProvider: 'serviceAccount', url: 'https://localhost:1234', + authMetadata: { authProvider: 'serviceAccount' }, }, { name: 'some-other-cluster', url: 'https://localhost:1235', - authProvider: 'google', + authMetadata: { authProvider: 'google' }, }, ]; const clusterSupplier: KubernetesClustersSupplier = { @@ -189,15 +189,15 @@ describe('KubernetesBuilder', () => { const logger = getVoidLogger(); const someCluster: ClusterDetails = { name: 'some-cluster', - authProvider: 'serviceAccount', url: 'https://localhost:1234', + authMetadata: { authProvider: 'serviceAccount' }, }; const clusters: ClusterDetails[] = [ someCluster, { name: 'some-other-cluster', url: 'https://localhost:1235', - authProvider: 'google', + authMetadata: { authProvider: 'google' }, }, ]; const clusterSupplier: KubernetesClustersSupplier = { diff --git a/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts b/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts index 02ea54a1d5..e9e631464c 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts @@ -225,6 +225,7 @@ export class KubernetesBuilder { this.clusterSupplier = getCombinedClusterSupplier( config, this.env.catalogApi, + new DispatchStrategy({ authStrategyMap: this.getAuthStrategyMap() }), refreshInterval, ); @@ -340,11 +341,11 @@ export class KubernetesBuilder { const clusterDetails = await this.fetchClusterDetails(clusterSupplier); res.json({ items: clusterDetails.map(cd => { - const oidcTokenProvider = cd.authMetadata?.oidcTokenProvider; + const oidcTokenProvider = cd.authMetadata.oidcTokenProvider; return { name: cd.name, dashboardUrl: cd.dashboardUrl, - authProvider: cd.authProvider, + authProvider: cd.authMetadata.authProvider, ...(oidcTokenProvider && { oidcTokenProvider }), }; }), @@ -358,14 +359,14 @@ export class KubernetesBuilder { protected buildAuthStrategyMap() { this.authStrategyMap = { - google: new GoogleStrategy(), aks: new AksStrategy(), aws: new AwsIamStrategy({ config: this.env.config }), azure: new AzureIdentityStrategy(this.env.logger), - serviceAccount: new NoopStrategy(), + google: new GoogleStrategy(), googleServiceAccount: new GoogleServiceAccountStrategy(), - oidc: new OidcStrategy(), localKubectlProxy: new NoopStrategy(), + oidc: new OidcStrategy(), + serviceAccount: new NoopStrategy(), }; return this.authStrategyMap; } diff --git a/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.test.ts b/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.test.ts index 1748beb006..8fc22946d4 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.test.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.test.ts @@ -34,7 +34,10 @@ import { Entity } from '@backstage/catalog-model'; describe('KubernetesFanOutHandler', () => { const fetchObjectsForService = jest.fn(); const fetchPodMetricsByNamespaces = jest.fn(); - const getClustersByEntity = jest.fn(); + const getClustersByEntity = jest.fn< + Promise<{ clusters: ClusterDetails[] }>, + [Entity] + >(); let config: Config; let sut: KubernetesFanOutHandler; @@ -73,7 +76,8 @@ describe('KubernetesFanOutHandler', () => { const cluster1 = { name: 'test-cluster', - authProvider: 'serviceAccount', + url: '', + authMetadata: { authProvider: 'serviceAccount' }, customResources: [ { group: 'some-other-crd.example.com', @@ -85,7 +89,8 @@ describe('KubernetesFanOutHandler', () => { const cluster2 = { name: 'cluster-two', - authProvider: 'serviceAccount', + url: '', + authMetadata: { authProvider: 'serviceAccount' }, customResources: [ { group: 'crd-two.example.com', @@ -183,6 +188,7 @@ describe('KubernetesFanOutHandler', () => { decorateClusterDetailsWithAuth: async (clusterDetails, _) => { return clusterDetails; }, + validate: jest.fn(), }, config, }); @@ -345,7 +351,8 @@ describe('KubernetesFanOutHandler', () => { clusters: [ { name: 'test-cluster', - authProvider: 'serviceAccount', + url: '', + authMetadata: { authProvider: 'serviceAccount' }, }, ], }), @@ -459,7 +466,8 @@ describe('KubernetesFanOutHandler', () => { clusters: [ { name: 'test-cluster', - authProvider: 'serviceAccount', + url: '', + authMetadata: { authProvider: 'serviceAccount' }, }, cluster2, ], @@ -513,7 +521,8 @@ describe('KubernetesFanOutHandler', () => { clusters: [ { name: 'profile-cluster-1', - authProvider: 'serviceAccount', + url: '', + authMetadata: { authProvider: 'serviceAccount' }, customResourceProfile: 'build', customResources: [ { @@ -561,7 +570,8 @@ describe('KubernetesFanOutHandler', () => { clusters: [ { name: 'profile-cluster-1', - authProvider: 'serviceAccount', + url: '', + authMetadata: { authProvider: 'serviceAccount' }, }, ], }), @@ -601,7 +611,8 @@ describe('KubernetesFanOutHandler', () => { clusters: [ { name: 'test-cluster', - authProvider: 'serviceAccount', + url: '', + authMetadata: { authProvider: 'serviceAccount' }, }, ], }), @@ -698,7 +709,8 @@ describe('KubernetesFanOutHandler', () => { clusters: [ { name: 'test-cluster', - authProvider: 'serviceAccount', + url: '', + authMetadata: { authProvider: 'serviceAccount' }, }, ], }), @@ -737,12 +749,14 @@ describe('KubernetesFanOutHandler', () => { clusters: [ { name: 'test-cluster', - authProvider: 'serviceAccount', + url: '', + authMetadata: { authProvider: 'serviceAccount' }, dashboardUrl: 'https://k8s.foo.coom', }, { name: 'other-cluster', - authProvider: 'google', + url: '', + authMetadata: { authProvider: 'google' }, }, ], }), @@ -788,15 +802,18 @@ describe('KubernetesFanOutHandler', () => { clusters: [ { name: 'test-cluster', - authProvider: 'serviceAccount', + url: '', + authMetadata: { authProvider: 'serviceAccount' }, }, { name: 'other-cluster', - authProvider: 'google', + url: '', + authMetadata: { authProvider: 'google' }, }, { name: 'empty-cluster', - authProvider: 'google', + url: '', + authMetadata: { authProvider: 'google' }, }, ], }), @@ -841,19 +858,23 @@ describe('KubernetesFanOutHandler', () => { clusters: [ { name: 'test-cluster', - authProvider: 'serviceAccount', + url: '', + authMetadata: { authProvider: 'serviceAccount' }, }, { name: 'other-cluster', - authProvider: 'google', + url: '', + authMetadata: { authProvider: 'google' }, }, { name: 'empty-cluster', - authProvider: 'google', + url: '', + authMetadata: { authProvider: 'google' }, }, { name: 'error-cluster', - authProvider: 'google', + url: '', + authMetadata: { authProvider: 'google' }, }, ], }), @@ -919,12 +940,14 @@ describe('KubernetesFanOutHandler', () => { clusters: [ { name: 'test-cluster', - authProvider: 'serviceAccount', + url: '', + authMetadata: { authProvider: 'serviceAccount' }, dashboardUrl: 'https://k8s.foo.coom', }, { name: 'other-cluster', - authProvider: 'google', + url: '', + authMetadata: { authProvider: 'google' }, }, ], }), @@ -1017,7 +1040,8 @@ describe('KubernetesFanOutHandler', () => { clusters: [ { name: 'test-cluster', - authProvider: 'serviceAccount', + url: '', + authMetadata: { authProvider: 'serviceAccount' }, skipMetricsLookup: true, }, ], @@ -1066,16 +1090,20 @@ describe('KubernetesFanOutHandler', () => { { name: 'works', url: 'https://works', - authProvider: 'serviceAccount', + authMetadata: { + authProvider: 'serviceAccount', + serviceAccountToken: 'token', + }, skipMetricsLookup: true, - authMetadata: { serviceAccountToken: 'token' }, }, { name: 'fails', url: 'https://fails', - authProvider: 'serviceAccount', + authMetadata: { + authProvider: 'serviceAccount', + serviceAccountToken: 'token', + }, skipMetricsLookup: true, - authMetadata: { serviceAccountToken: 'token' }, }, ], }), @@ -1104,6 +1132,7 @@ describe('KubernetesFanOutHandler', () => { decorateClusterDetailsWithAuth: async (clusterDetails, _) => { return clusterDetails; }, + validate: jest.fn(), }, config, }); @@ -1199,7 +1228,8 @@ describe('KubernetesFanOutHandler', () => { clusters: [ { name: 'test-cluster', - authProvider: 'serviceAccount', + url: '', + authMetadata: { authProvider: 'serviceAccount' }, }, cluster2, ], @@ -1272,7 +1302,8 @@ describe('KubernetesFanOutHandler', () => { clusters: [ { name: 'profile-cluster-1', - authProvider: 'serviceAccount', + url: '', + authMetadata: { authProvider: 'serviceAccount' }, customResourceProfile: 'build', }, ], diff --git a/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts b/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts index 37f134be46..0c47c59184 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts @@ -138,8 +138,10 @@ describe('KubernetesFetcher', () => { clusterDetails: { name: 'cluster1', url: 'http://localhost:9999', - authProvider: 'serviceAccount', - authMetadata: { serviceAccountToken: 'token' }, + authMetadata: { + authProvider: 'serviceAccount', + serviceAccountToken: 'token', + }, }, objectTypesToFetch: OBJECTS_TO_FETCH, labelSelector: '', @@ -199,8 +201,10 @@ describe('KubernetesFetcher', () => { clusterDetails: { name: 'cluster1', url: 'http://localhost:9999/k8s/clusters/1234', - authProvider: 'serviceAccount', - authMetadata: { serviceAccountToken: 'token' }, + authMetadata: { + authProvider: 'serviceAccount', + serviceAccountToken: 'token', + }, }, objectTypesToFetch: OBJECTS_TO_FETCH, labelSelector: '', @@ -253,7 +257,7 @@ describe('KubernetesFetcher', () => { clusterDetails: { name: 'cluster1', url: 'http://localhost:9999/k8s/clusters/1234', - authProvider: 'localKubectlProxy', + authMetadata: { authProvider: 'localKubectlProxy' }, }, objectTypesToFetch: new Set([ { @@ -309,8 +313,10 @@ describe('KubernetesFetcher', () => { clusterDetails: { name: 'cluster1', url: 'http://localhost:9999', - authProvider: 'serviceAccount', - authMetadata: { serviceAccountToken: 'token' }, + authMetadata: { + authProvider: 'serviceAccount', + serviceAccountToken: 'token', + }, }, objectTypesToFetch: OBJECTS_TO_FETCH, labelSelector: '', @@ -383,8 +389,10 @@ describe('KubernetesFetcher', () => { clusterDetails: { name: 'cluster1', url: 'http://localhost:9999', - authProvider: 'serviceAccount', - authMetadata: { serviceAccountToken: 'token' }, + authMetadata: { + authProvider: 'serviceAccount', + serviceAccountToken: 'token', + }, }, objectTypesToFetch: OBJECTS_TO_FETCH, labelSelector: '', @@ -475,8 +483,10 @@ describe('KubernetesFetcher', () => { clusterDetails: { name: 'cluster1', url: 'http://localhost:9999', - authProvider: 'serviceAccount', - authMetadata: { serviceAccountToken: 'token' }, + authMetadata: { + authProvider: 'serviceAccount', + serviceAccountToken: 'token', + }, }, objectTypesToFetch: OBJECTS_TO_FETCH, labelSelector: '', @@ -563,8 +573,10 @@ describe('KubernetesFetcher', () => { clusterDetails: { name: 'cluster1', url: 'http://badurl.does.not.exist', - authProvider: 'serviceAccount', - authMetadata: { serviceAccountToken: 'token' }, + authMetadata: { + authProvider: 'serviceAccount', + serviceAccountToken: 'token', + }, }, objectTypesToFetch: OBJECTS_TO_FETCH, labelSelector: '', @@ -600,8 +612,10 @@ describe('KubernetesFetcher', () => { clusterDetails: { name: 'cluster1', url: 'http://localhost:9999', - authProvider: 'serviceAccount', - authMetadata: { serviceAccountToken: 'token' }, + authMetadata: { + authProvider: 'serviceAccount', + serviceAccountToken: 'token', + }, }, objectTypesToFetch: OBJECTS_TO_FETCH, labelSelector: 'service-label=value', @@ -667,8 +681,10 @@ describe('KubernetesFetcher', () => { clusterDetails: { name: 'cluster1', url: 'https://localhost:9999', - authProvider: 'serviceAccount', - authMetadata: { serviceAccountToken: 'token' }, + authMetadata: { + authProvider: 'serviceAccount', + serviceAccountToken: 'token', + }, caData: 'MOCKCA', }, objectTypesToFetch: new Set([ @@ -704,8 +720,10 @@ describe('KubernetesFetcher', () => { clusterDetails: { name: 'cluster1', url: 'https://localhost:9999', - authProvider: 'serviceAccount', - authMetadata: { serviceAccountToken: 'token' }, + authMetadata: { + authProvider: 'serviceAccount', + serviceAccountToken: 'token', + }, }, objectTypesToFetch: new Set([ { @@ -747,8 +765,10 @@ describe('KubernetesFetcher', () => { clusterDetails: { name: 'cluster1', url: 'https://localhost:9999', - authProvider: 'serviceAccount', - authMetadata: { serviceAccountToken: 'token' }, + authMetadata: { + authProvider: 'serviceAccount', + serviceAccountToken: 'token', + }, caFile: '/path/to/ca.crt', }, objectTypesToFetch: new Set([ @@ -785,8 +805,10 @@ describe('KubernetesFetcher', () => { clusterDetails: { name: 'cluster1', url: 'https://localhost:9999', - authProvider: 'serviceAccount', - authMetadata: { serviceAccountToken: 'token' }, + authMetadata: { + authProvider: 'serviceAccount', + serviceAccountToken: 'token', + }, skipTLSVerify: true, }, objectTypesToFetch: new Set([ @@ -836,8 +858,10 @@ describe('KubernetesFetcher', () => { clusterDetails: { name: 'cluster1', url: 'http://localhost:9999', - authProvider: 'serviceAccount', - authMetadata: { serviceAccountToken: 'token' }, + authMetadata: { + authProvider: 'serviceAccount', + serviceAccountToken: 'token', + }, }, objectTypesToFetch: OBJECTS_TO_FETCH, labelSelector: '', @@ -880,7 +904,7 @@ describe('KubernetesFetcher', () => { clusterDetails: { name: 'unauthenticated-cluster', url: 'http://ignored', - authProvider: 'serviceAccount', + authMetadata: { authProvider: 'serviceAccount' }, }, objectTypesToFetch: OBJECTS_TO_FETCH, labelSelector: '', @@ -923,7 +947,7 @@ describe('KubernetesFetcher', () => { clusterDetails: { name: 'overridden-to-in-cluster', url: 'http://ignored', - authProvider: 'serviceAccount', + authMetadata: { authProvider: 'serviceAccount' }, }, objectTypesToFetch: new Set([ { @@ -1019,8 +1043,10 @@ describe('KubernetesFetcher', () => { { name: 'cluster1', url: 'http://localhost:9999', - authProvider: 'serviceAccount', - authMetadata: { serviceAccountToken: 'token' }, + authMetadata: { + authProvider: 'serviceAccount', + serviceAccountToken: 'token', + }, }, new Set(['ns-a']), ); @@ -1106,8 +1132,10 @@ describe('KubernetesFetcher', () => { { name: 'cluster1', url: 'http://localhost:9999', - authProvider: 'serviceAccount', - authMetadata: { serviceAccountToken: 'token' }, + authMetadata: { + authProvider: 'serviceAccount', + serviceAccountToken: 'token', + }, }, new Set(['ns-a', 'ns-b']), ); diff --git a/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts b/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts index f3499f039e..167452d362 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts @@ -201,8 +201,8 @@ export class KubernetesClientBasedFetcher implements KubernetesFetcher { let url: URL; let requestInit: RequestInit; if ( - clusterDetails?.authMetadata?.serviceAccountToken || - clusterDetails.authProvider === 'localKubectlProxy' + clusterDetails.authMetadata.serviceAccountToken || + clusterDetails.authMetadata.authProvider === 'localKubectlProxy' ) { [url, requestInit] = this.fetchArgsFromClusterDetails(clusterDetails); } else if (fs.pathExistsSync(Config.SERVICEACCOUNT_TOKEN_PATH)) { @@ -236,7 +236,7 @@ export class KubernetesClientBasedFetcher implements KubernetesFetcher { headers: { Accept: 'application/json', 'Content-Type': 'application/json', - Authorization: `Bearer ${clusterDetails.authMetadata?.serviceAccountToken}`, + Authorization: `Bearer ${clusterDetails.authMetadata.serviceAccountToken}`, }, }; diff --git a/plugins/kubernetes-backend/src/service/KubernetesProxy.test.ts b/plugins/kubernetes-backend/src/service/KubernetesProxy.test.ts index 6fc5c26620..7f28ceff4e 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesProxy.test.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesProxy.test.ts @@ -61,6 +61,7 @@ describe('KubernetesProxy', () => { const authStrategy: jest.Mocked = { decorateClusterDetailsWithAuth: jest.fn(), + validate: jest.fn(), }; setupRequestMockHandlers(worker); @@ -145,14 +146,16 @@ describe('KubernetesProxy', () => { { name: 'local', url: 'http:/localhost:8001', - authProvider: 'localKubectlProxy', + authMetadata: { authProvider: 'localKubectlProxy' }, skipMetricsLookup: true, } as ClusterDetails, { name: 'cluster1', url: 'https://localhost:9999', - authProvider: 'googleServiceAccount', - authMetadata: { serviceAccountToken: 'tokenA' }, + authMetadata: { + authProvider: 'googleServiceAccount', + serviceAccountToken: 'tokenA', + }, } as ClusterDetails, ]); @@ -169,8 +172,10 @@ describe('KubernetesProxy', () => { { name: 'cluster1', url: 'https://localhost:9999', - authProvider: 'googleServiceAccount', - authMetadata: { serviceAccountToken: 'tokenA' }, + authMetadata: { + authProvider: 'googleServiceAccount', + serviceAccountToken: 'tokenA', + }, } as ClusterDetails, ]); @@ -198,14 +203,14 @@ describe('KubernetesProxy', () => { { name: 'cluster1', url: 'https://localhost:9999', - authProvider: 'serviceAccount', + authMetadata: { authProvider: 'serviceAccount' }, }, ] as ClusterDetails[]); authStrategy.decorateClusterDetailsWithAuth.mockResolvedValue({ name: 'cluster1', url: 'https://localhost:9999', - authProvider: 'serviceAccount', + authMetadata: { authProvider: 'serviceAccount' }, } as ClusterDetails); worker.use( @@ -242,14 +247,14 @@ describe('KubernetesProxy', () => { { name: 'cluster1', url: 'https://localhost:9999', - authProvider: 'serviceAccount', + authMetadata: { authProvider: 'serviceAccount' }, }, ] as ClusterDetails[]); authStrategy.decorateClusterDetailsWithAuth.mockResolvedValue({ name: 'cluster1', url: 'https://localhost:9999', - authProvider: 'serviceAccount', + authMetadata: { authProvider: 'serviceAccount' }, } as ClusterDetails); worker.use( @@ -286,7 +291,7 @@ describe('KubernetesProxy', () => { { name: 'cluster1', url: 'http://localhost:9999', - authProvider: '', + authMetadata: { authProvider: '' }, }, ]); authStrategy.decorateClusterDetailsWithAuth.mockImplementation( @@ -337,15 +342,17 @@ describe('KubernetesProxy', () => { { name: 'cluster1', url: 'https://localhost:9999', - authProvider: 'serviceAccount', + authMetadata: { authProvider: 'serviceAccount' }, }, ] as ClusterDetails[]); authStrategy.decorateClusterDetailsWithAuth.mockResolvedValue({ name: 'cluster1', url: 'https://localhost:9999', - authProvider: 'serviceAccount', - authMetadata: { serviceAccountToken: 'strategy-provided-token' }, + authMetadata: { + authProvider: 'serviceAccount', + serviceAccountToken: 'strategy-provided-token', + }, } as ClusterDetails); const requestPromise = setupProxyPromise({ @@ -386,15 +393,17 @@ describe('KubernetesProxy', () => { { name: 'cluster1', url: 'https://localhost:9999', - authProvider: 'googleServiceAccount', + authMetadata: { authProvider: 'googleServiceAccount' }, }, ] as ClusterDetails[]); authStrategy.decorateClusterDetailsWithAuth.mockResolvedValue({ name: 'cluster1', url: 'https://localhost:9999', - authProvider: 'googleServiceAccount', - authMetadata: { serviceAccountToken: 'my-token' }, + authMetadata: { + authProvider: 'googleServiceAccount', + serviceAccountToken: 'my-token', + }, } as ClusterDetails); const requestPromise = setupProxyPromise({ @@ -440,15 +449,17 @@ describe('KubernetesProxy', () => { { name: 'cluster1', url: 'https://localhost:9999', - authProvider: 'googleServiceAccount', + authMetadata: { authProvider: 'googleServiceAccount' }, }, ] as ClusterDetails[]); authStrategy.decorateClusterDetailsWithAuth.mockResolvedValue({ name: 'cluster1', url: 'https://localhost:9999', - authProvider: 'googleServiceAccount', - authMetadata: { serviceAccountToken: 'tokenA' }, + authMetadata: { + authProvider: 'googleServiceAccount', + serviceAccountToken: 'tokenA', + }, } as ClusterDetails); const requestPromise = setupProxyPromise({ @@ -497,7 +508,7 @@ describe('KubernetesProxy', () => { { name: 'cluster1', url: 'https://localhost:9999', - authProvider: 'googleServiceAccount', + authMetadata: { authProvider: 'googleServiceAccount' }, }, ] as ClusterDetails[]); @@ -591,8 +602,10 @@ describe('KubernetesProxy', () => { { name: 'cluster1', url: 'https://localhost:9999', - authProvider: 'google', - authMetadata: { serviceAccountToken: 'client-side-token' }, + authMetadata: { + authProvider: 'google', + serviceAccountToken: 'client-side-token', + }, }, ] as ClusterDetails[]); @@ -635,7 +648,7 @@ describe('KubernetesProxy', () => { { name: 'cluster1', url: 'http://localhost:9999/subpath', - authProvider: '', + authMetadata: { authProvider: '' }, }, ]); @@ -780,14 +793,14 @@ describe('KubernetesProxy', () => { { name: 'local', url: `http://localhost:${wsPort}`, - authProvider: 'serviceAccount', + authMetadata: { authProvider: 'serviceAccount' }, }, ] as ClusterDetails[]); authStrategy.decorateClusterDetailsWithAuth.mockResolvedValue({ name: 'local', url: `http://localhost:${wsPort}`, - authProvider: 'serviceAccount', + authMetadata: { authProvider: 'serviceAccount' }, } as ClusterDetails); const wsProxyAddress = `ws://127.0.0.1:${proxyPort}${proxyPath}${wsPath}`; diff --git a/plugins/kubernetes-backend/src/service/KubernetesProxy.ts b/plugins/kubernetes-backend/src/service/KubernetesProxy.ts index 07753a2517..d895a38e6e 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesProxy.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesProxy.ts @@ -114,7 +114,7 @@ export class KubernetesProxy { } else { const serviceAccountToken = await this.getClusterForRequest(req) .then(cd => this.authStrategy.decorateClusterDetailsWithAuth(cd, {})) - .then(cd => cd?.authMetadata?.serviceAccountToken); + .then(cd => cd.authMetadata.serviceAccountToken); if (serviceAccountToken) { req.headers.authorization = `Bearer ${serviceAccountToken}`; } diff --git a/plugins/kubernetes-backend/src/types/types.ts b/plugins/kubernetes-backend/src/types/types.ts index 890ebd73a3..48bed695d0 100644 --- a/plugins/kubernetes-backend/src/types/types.ts +++ b/plugins/kubernetes-backend/src/types/types.ts @@ -144,6 +144,12 @@ export interface KubernetesServiceLocator { */ export type ServiceLocatorMethod = 'multiTenant' | 'http'; // TODO implement http +/** + * Provider-specific authentication configuration + * @public + */ +export type AuthMetadata = Record; + /** * * @public @@ -154,11 +160,7 @@ export interface ClusterDetails { */ name: string; url: string; - authProvider: string; - /** - * Provider-specific authentication configuration - */ - authMetadata?: Record; + authMetadata: AuthMetadata; skipTLSVerify?: boolean; /** * Whether to skip the lookup to the metrics server to retrieve pod resource usage. From efc0d893cd04315de7493155c53a1471879fd835 Mon Sep 17 00:00:00 2001 From: Jamie Klassen Date: Thu, 7 Sep 2023 09:53:12 -0400 Subject: [PATCH 12/20] rearrange clusterlocator tests and remove default authStrategy parameter Signed-off-by: Jamie Klassen --- .../src/auth/OidcStrategy.test.ts | 97 ++++++----- .../ConfigClusterLocator.test.ts | 159 +++++++----------- .../cluster-locator/ConfigClusterLocator.ts | 24 +-- 3 files changed, 117 insertions(+), 163 deletions(-) diff --git a/plugins/kubernetes-backend/src/auth/OidcStrategy.test.ts b/plugins/kubernetes-backend/src/auth/OidcStrategy.test.ts index 5650e0eccd..7843cc6544 100644 --- a/plugins/kubernetes-backend/src/auth/OidcStrategy.test.ts +++ b/plugins/kubernetes-backend/src/auth/OidcStrategy.test.ts @@ -15,47 +15,18 @@ */ import { ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER } from '@backstage/plugin-kubernetes-common'; +import { AuthMetadata } from '../types/types'; import { OidcStrategy } from './OidcStrategy'; -describe('OidcStrategy tests', () => { - const strategy = new OidcStrategy(); - - it('returns cluster details with auth token', async () => { - const details = await strategy.decorateClusterDetailsWithAuth( - { - name: 'test', - url: '', - authMetadata: { - authProvider: 'oidc', - [ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER]: 'okta', - }, - }, - { - oidc: { okta: 'fakeToken' }, - }, - ); - - expect(details.authMetadata.serviceAccountToken).toBe('fakeToken'); +describe('OidcStrategy', () => { + let strategy: OidcStrategy; + beforeEach(() => { + strategy = new OidcStrategy(); }); - it('returns error when oidcTokenProvider is not configured', async () => { - await expect( - strategy.decorateClusterDetailsWithAuth( - { - name: 'test', - url: '', - authMetadata: { authProvider: 'oidc' }, - }, - {}, - ), - ).rejects.toThrow( - 'oidc authProvider requires a configured oidcTokenProvider', - ); - }); - - it('returns error when token is not included in request body', async () => { - await expect( - strategy.decorateClusterDetailsWithAuth( + describe('decorateClusterDetailsWithAuth', () => { + it('returns cluster details with auth token', async () => { + const details = await strategy.decorateClusterDetailsWithAuth( { name: 'test', url: '', @@ -64,8 +35,54 @@ describe('OidcStrategy tests', () => { [ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER]: 'okta', }, }, - {}, - ), - ).rejects.toThrow('Auth token not found under oidc.okta in request body'); + { + oidc: { okta: 'fakeToken' }, + }, + ); + + expect(details.authMetadata.serviceAccountToken).toBe('fakeToken'); + }); + + it('fails when oidcTokenProvider is not configured', async () => { + await expect( + strategy.decorateClusterDetailsWithAuth( + { + name: 'test', + url: '', + authMetadata: { authProvider: 'oidc' }, + }, + {}, + ), + ).rejects.toThrow( + 'oidc authProvider requires a configured oidcTokenProvider', + ); + }); + + it('fails when token is not included in request body', async () => { + await expect( + strategy.decorateClusterDetailsWithAuth( + { + name: 'test', + url: '', + authMetadata: { + authProvider: 'oidc', + [ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER]: 'okta', + }, + }, + {}, + ), + ).rejects.toThrow('Auth token not found under oidc.okta in request body'); + }); + }); + + describe('validate', () => { + it('fails when token provider is not specified', () => { + const authMetadata: AuthMetadata = { + authProvider: 'oidc', + }; + expect(() => strategy.validate(authMetadata)).toThrow( + `Must specify a token provider for 'oidc' strategy`, + ); + }); }); }); diff --git a/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.test.ts b/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.test.ts index 58e21909f7..8c0d565f96 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.test.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.test.ts @@ -22,14 +22,24 @@ import { } from '@backstage/plugin-kubernetes-common'; import { ConfigClusterLocator } from './ConfigClusterLocator'; import { ClusterDetails } from '../types/types'; +import { AuthenticationStrategy } from '../auth'; describe('ConfigClusterLocator', () => { + let authStrategy: jest.Mocked; + + beforeEach(() => { + authStrategy = { + decorateClusterDetailsWithAuth: jest.fn(), + validate: jest.fn(), + }; + }); + it('empty clusters returns empty cluster details', async () => { const config: Config = new ConfigReader({ clusters: [], }); - const sut = ConfigClusterLocator.fromConfig(config); + const sut = ConfigClusterLocator.fromConfig(config, authStrategy); const result = await sut.getClusters(); @@ -47,7 +57,7 @@ describe('ConfigClusterLocator', () => { ], }); - const sut = ConfigClusterLocator.fromConfig(config); + const sut = ConfigClusterLocator.fromConfig(config, authStrategy); const result = await sut.getClusters(); @@ -86,7 +96,7 @@ describe('ConfigClusterLocator', () => { ], }); - const sut = ConfigClusterLocator.fromConfig(config); + const sut = ConfigClusterLocator.fromConfig(config, authStrategy); const result = await sut.getClusters(); @@ -116,75 +126,50 @@ describe('ConfigClusterLocator', () => { ]); }); - it('one aws cluster with assumeRole and one without', async () => { + it('copies "assumeRole" config value into metadata', async () => { const config: Config = new ConfigReader({ clusters: [ { name: 'cluster1', - serviceAccountToken: 'token', url: 'http://localhost:8080', authProvider: 'aws', - skipTLSVerify: false, - }, - { assumeRole: 'SomeRole', - name: 'cluster2', - url: 'http://localhost:8081', - authProvider: 'aws', - skipTLSVerify: true, - }, - { - assumeRole: 'SomeRole', - name: 'cluster2', - externalId: 'SomeExternalId', - url: 'http://localhost:8081', - authProvider: 'aws', - skipTLSVerify: true, }, ], }); - const sut = ConfigClusterLocator.fromConfig(config); - + const sut = ConfigClusterLocator.fromConfig(config, authStrategy); const result = await sut.getClusters(); - expect(result).toStrictEqual([ + expect(result).toMatchObject([ { - name: 'cluster1', - url: 'http://localhost:8080', - authMetadata: { - authProvider: 'aws', - serviceAccountToken: 'token', - }, - skipTLSVerify: false, - skipMetricsLookup: false, - caData: undefined, - caFile: undefined, - }, - { - name: 'cluster2', - url: 'http://localhost:8081', - authMetadata: { - authProvider: 'aws', + authMetadata: expect.objectContaining({ [ANNOTATION_KUBERNETES_AWS_ASSUME_ROLE]: 'SomeRole', - }, - skipTLSVerify: true, - skipMetricsLookup: false, - caData: undefined, - caFile: undefined, + }), }, - { - name: 'cluster2', - url: 'http://localhost:8081', - authMetadata: { + ]); + }); + + it('copies "externalId" config value into metadata', async () => { + const config: Config = new ConfigReader({ + clusters: [ + { + name: 'cluster1', + url: 'http://localhost:8080', authProvider: 'aws', - [ANNOTATION_KUBERNETES_AWS_ASSUME_ROLE]: 'SomeRole', + externalId: 'SomeExternalId', + }, + ], + }); + + const sut = ConfigClusterLocator.fromConfig(config, authStrategy); + const result = await sut.getClusters(); + + expect(result).toMatchObject([ + { + authMetadata: expect.objectContaining({ [ANNOTATION_KUBERNETES_AWS_EXTERNAL_ID]: 'SomeExternalId', - }, - skipTLSVerify: true, - skipMetricsLookup: false, - caData: undefined, - caFile: undefined, + }), }, ]); }); @@ -206,7 +191,7 @@ describe('ConfigClusterLocator', () => { ], }); - const sut = ConfigClusterLocator.fromConfig(config); + const sut = ConfigClusterLocator.fromConfig(config, authStrategy); const result = await sut.getClusters(); @@ -242,7 +227,7 @@ describe('ConfigClusterLocator', () => { ], }); - const sut = ConfigClusterLocator.fromConfig(config); + const sut = ConfigClusterLocator.fromConfig(config, authStrategy); const result = await sut.getClusters(); @@ -261,34 +246,6 @@ describe('ConfigClusterLocator', () => { ]); }); - it('supports aks authProvider', async () => { - const sut = ConfigClusterLocator.fromConfig( - new ConfigReader({ - clusters: [ - { - name: 'aks-cluster', - url: 'https://aks.test', - authProvider: 'aks', - }, - ], - }), - ); - - const result = await sut.getClusters(); - - expect(result).toStrictEqual([ - { - name: 'aks-cluster', - url: 'https://aks.test', - authMetadata: { authProvider: 'aks' }, - skipMetricsLookup: false, - skipTLSVerify: false, - caData: undefined, - caFile: undefined, - }, - ]); - }); - it('has cluster level defined customResources returns clusterDetails with those CRDs', async () => { const config: Config = new ConfigReader({ clusters: [ @@ -307,7 +264,7 @@ describe('ConfigClusterLocator', () => { ], }); - const sut = ConfigClusterLocator.fromConfig(config); + const sut = ConfigClusterLocator.fromConfig(config, authStrategy); const result = await sut.getClusters(); @@ -331,22 +288,22 @@ describe('ConfigClusterLocator', () => { ]); }); - // TODO move this to a test on OidcStrategy#validate - it('errors when authProvider is oidc but oidcTokenProvider is missing', async () => { - expect(() => - ConfigClusterLocator.fromConfig( - new ConfigReader({ - clusters: [ - { - name: 'oidc-cluster', - url: 'https://aks.test', - authProvider: 'oidc', - }, - ], - }), - ), - ).toThrow( - `Invalid cluster 'oidc-cluster': Must specify a token provider for 'oidc' strategy`, + it('wraps validation errors from auth strategy', () => { + const config: Config = new ConfigReader({ + clusters: [ + { + name: 'cluster1', + url: 'http://localhost:8080', + authProvider: 'authProvider', + }, + ], + }); + authStrategy.validate.mockImplementation(_ => { + throw new Error('mock error'); + }); + + expect(() => ConfigClusterLocator.fromConfig(config, authStrategy)).toThrow( + `Invalid cluster 'cluster1': mock error`, ); }); }); diff --git a/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.ts b/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.ts index bba9a69635..4bf6f44f79 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.ts @@ -21,25 +21,7 @@ import { ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER, } from '@backstage/plugin-kubernetes-common'; import { ClusterDetails, KubernetesClustersSupplier } from '../types/types'; -import { - AuthenticationStrategy, - AksStrategy, - DispatchStrategy, - GoogleStrategy, - GoogleServiceAccountStrategy, - NoopStrategy, - OidcStrategy, -} from '../auth'; - -const defaultAuthStrategies: Record = { - aks: new AksStrategy(), - aws: new NoopStrategy(), - google: new GoogleStrategy(), - googleServiceAccount: new GoogleServiceAccountStrategy(), - localKubectlProxy: new NoopStrategy(), - oidc: new OidcStrategy(), - serviceAccount: new NoopStrategy(), -}; +import { AuthenticationStrategy } from '../auth'; export class ConfigClusterLocator implements KubernetesClustersSupplier { private readonly clusterDetails: ClusterDetails[]; @@ -50,9 +32,7 @@ export class ConfigClusterLocator implements KubernetesClustersSupplier { static fromConfig( config: Config, - authStrategy: AuthenticationStrategy = new DispatchStrategy({ - authStrategyMap: defaultAuthStrategies, - }), + authStrategy: AuthenticationStrategy, ): ConfigClusterLocator { return new ConfigClusterLocator( config.getConfigArray('clusters').map(c => { From 279adfc6fd8234fed43ad06f5e6ad1d24c0557a8 Mon Sep 17 00:00:00 2001 From: Jamie Klassen Date: Thu, 7 Sep 2023 10:39:08 -0400 Subject: [PATCH 13/20] use annotation key authProvider in metadata This allows the nice simplification of the catalog clusterLocator -- just read the entire annotations block as auth metadata. Signed-off-by: Jamie Klassen --- .../src/auth/DispatchStrategy.test.ts | 13 ++++++---- .../src/auth/DispatchStrategy.ts | 13 ++++++---- .../CatalogClusterLocator.test.ts | 11 ++++----- .../cluster-locator/CatalogClusterLocator.ts | 10 +------- .../ConfigClusterLocator.test.ts | 21 +++++++++++----- .../cluster-locator/ConfigClusterLocator.ts | 3 ++- .../src/cluster-locator/index.test.ts | 14 ++++------- .../src/service/KubernetesBuilder.test.ts | 24 ++++++++++++++----- .../src/service/KubernetesBuilder.ts | 11 ++++++--- 9 files changed, 71 insertions(+), 49 deletions(-) diff --git a/plugins/kubernetes-backend/src/auth/DispatchStrategy.test.ts b/plugins/kubernetes-backend/src/auth/DispatchStrategy.test.ts index 07adff946a..d791972421 100644 --- a/plugins/kubernetes-backend/src/auth/DispatchStrategy.test.ts +++ b/plugins/kubernetes-backend/src/auth/DispatchStrategy.test.ts @@ -14,9 +14,12 @@ * limitations under the License. */ +import { + ANNOTATION_KUBERNETES_AUTH_PROVIDER, + KubernetesRequestAuth, +} from '@backstage/plugin-kubernetes-common'; import { DispatchStrategy } from './DispatchStrategy'; import { ClusterDetails } from '../types'; -import { KubernetesRequestAuth } from '@backstage/plugin-kubernetes-common'; import { AuthenticationStrategy } from './types'; describe('decorateClusterDetailsWithAuth', () => { @@ -39,7 +42,7 @@ describe('decorateClusterDetailsWithAuth', () => { url: 'notanything.com', name: 'randomName', authMetadata: { - authProvider: 'google', + [ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'google', serviceAccountToken: 'added by mock strategy', }, }; @@ -52,7 +55,7 @@ describe('decorateClusterDetailsWithAuth', () => { { name: 'googleCluster', url: 'anything.com', - authMetadata: { authProvider: 'google' }, + authMetadata: { [ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'google' }, }, authObject, ); @@ -61,7 +64,7 @@ describe('decorateClusterDetailsWithAuth', () => { { name: 'googleCluster', url: 'anything.com', - authMetadata: { authProvider: 'google' }, + authMetadata: { [ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'google' }, }, authObject, ); @@ -74,7 +77,7 @@ describe('decorateClusterDetailsWithAuth', () => { { name: 'test-cluster', url: 'anything.com', - authMetadata: { authProvider: 'linode' }, + authMetadata: { [ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'linode' }, }, authObject, ), diff --git a/plugins/kubernetes-backend/src/auth/DispatchStrategy.ts b/plugins/kubernetes-backend/src/auth/DispatchStrategy.ts index 6791bdbe3e..f4150ad8b6 100644 --- a/plugins/kubernetes-backend/src/auth/DispatchStrategy.ts +++ b/plugins/kubernetes-backend/src/auth/DispatchStrategy.ts @@ -16,7 +16,10 @@ import { AuthenticationStrategy } from './types'; import { AuthMetadata, ClusterDetails } from '../types'; -import { KubernetesRequestAuth } from '@backstage/plugin-kubernetes-common'; +import { + ANNOTATION_KUBERNETES_AUTH_PROVIDER, + KubernetesRequestAuth, +} from '@backstage/plugin-kubernetes-common'; /** * @@ -42,7 +45,8 @@ export class DispatchStrategy implements AuthenticationStrategy { clusterDetails: ClusterDetails, auth: KubernetesRequestAuth, ) { - const authProvider = clusterDetails.authMetadata.authProvider; + const authProvider = + clusterDetails.authMetadata[ANNOTATION_KUBERNETES_AUTH_PROVIDER]; if (this.strategyMap[authProvider]) { return this.strategyMap[authProvider].decorateClusterDetailsWithAuth( clusterDetails, @@ -55,10 +59,11 @@ export class DispatchStrategy implements AuthenticationStrategy { } public validate(authMetadata: AuthMetadata) { - const strategy = this.strategyMap[authMetadata.authProvider]; + const authProvider = authMetadata[ANNOTATION_KUBERNETES_AUTH_PROVIDER]; + const strategy = this.strategyMap[authProvider]; if (!strategy) { throw new Error( - `authProvider "${authMetadata.authProvider}" has no config associated with it`, + `authProvider "${authProvider}" has no config associated with it`, ); } strategy.validate(authMetadata); diff --git a/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.test.ts b/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.test.ts index 42424f4a0d..cd8195b9dc 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.test.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.test.ts @@ -16,6 +16,7 @@ import '@backstage/backend-common'; import { + ANNOTATION_KUBERNETES_AUTH_PROVIDER, ANNOTATION_KUBERNETES_AWS_ASSUME_ROLE, ANNOTATION_KUBERNETES_AWS_EXTERNAL_ID, ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER, @@ -35,7 +36,7 @@ const mockCatalogApi = { annotations: { 'kubernetes.io/api-server': 'https://apiserver.com', 'kubernetes.io/api-server-certificate-authority': 'caData', - 'kubernetes.io/auth-provider': 'oidc', + [ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'oidc', [ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER]: 'google', 'kubernetes.io/skip-metrics-lookup': 'true', 'kubernetes.io/skip-tls-verify': 'true', @@ -53,7 +54,7 @@ const mockCatalogApi = { annotations: { 'kubernetes.io/api-server': 'https://apiserver.com', 'kubernetes.io/api-server-certificate-authority': 'caData', - 'kubernetes.io/auth-provider': 'aws', + [ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'aws', [ANNOTATION_KUBERNETES_AWS_ASSUME_ROLE]: 'my-role', [ANNOTATION_KUBERNETES_AWS_EXTERNAL_ID]: 'my-id', [ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER]: 'google', @@ -97,10 +98,9 @@ describe('CatalogClusterLocator', () => { url: 'https://apiserver.com', caData: 'caData', authMetadata: { - authProvider: 'oidc', 'kubernetes.io/api-server': 'https://apiserver.com', 'kubernetes.io/api-server-certificate-authority': 'caData', - 'kubernetes.io/auth-provider': 'oidc', + [ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'oidc', [ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER]: 'google', 'kubernetes.io/skip-metrics-lookup': 'true', 'kubernetes.io/skip-tls-verify': 'true', @@ -125,10 +125,9 @@ describe('CatalogClusterLocator', () => { url: 'https://apiserver.com', caData: 'caData', authMetadata: { - authProvider: 'aws', 'kubernetes.io/api-server': 'https://apiserver.com', 'kubernetes.io/api-server-certificate-authority': 'caData', - 'kubernetes.io/auth-provider': 'aws', + [ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'aws', [ANNOTATION_KUBERNETES_AWS_ASSUME_ROLE]: 'my-role', [ANNOTATION_KUBERNETES_AWS_EXTERNAL_ID]: 'my-id', [ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER]: 'google', diff --git a/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.ts b/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.ts index b2499639b0..f8abd5b203 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.ts @@ -57,17 +57,9 @@ export class CatalogClusterLocator implements KubernetesClustersSupplier { const clusterDetails: ClusterDetails = { name: entity.metadata.name, url: entity.metadata.annotations![ANNOTATION_KUBERNETES_API_SERVER]!, + authMetadata: entity.metadata.annotations!, caData: entity.metadata.annotations![ANNOTATION_KUBERNETES_API_SERVER_CA]!, - ...{ - authMetadata: { - authProvider: - entity.metadata.annotations![ - ANNOTATION_KUBERNETES_AUTH_PROVIDER - ]!, - ...entity.metadata.annotations, - }, - }, skipMetricsLookup: entity.metadata.annotations![ ANNOTATION_KUBERNETES_SKIP_METRICS_LOOKUP diff --git a/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.test.ts b/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.test.ts index 8c0d565f96..73c81e0f59 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.test.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.test.ts @@ -17,6 +17,7 @@ import '@backstage/backend-common'; import { ConfigReader, Config } from '@backstage/config'; import { + ANNOTATION_KUBERNETES_AUTH_PROVIDER, ANNOTATION_KUBERNETES_AWS_ASSUME_ROLE, ANNOTATION_KUBERNETES_AWS_EXTERNAL_ID, } from '@backstage/plugin-kubernetes-common'; @@ -65,7 +66,9 @@ describe('ConfigClusterLocator', () => { { name: 'cluster1', url: 'http://localhost:8080', - authMetadata: { authProvider: 'serviceAccount' }, + authMetadata: { + [ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'serviceAccount', + }, skipMetricsLookup: false, skipTLSVerify: false, caData: undefined, @@ -106,7 +109,7 @@ describe('ConfigClusterLocator', () => { dashboardUrl: 'https://k8s.foo.com', url: 'http://localhost:8080', authMetadata: { - authProvider: 'serviceAccount', + [ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'serviceAccount', serviceAccountToken: 'token', }, skipTLSVerify: false, @@ -117,7 +120,7 @@ describe('ConfigClusterLocator', () => { { name: 'cluster2', url: 'http://localhost:8081', - authMetadata: { authProvider: 'google' }, + authMetadata: { [ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'google' }, skipTLSVerify: true, skipMetricsLookup: false, caData: undefined, @@ -199,7 +202,9 @@ describe('ConfigClusterLocator', () => { { name: 'cluster1', url: 'http://localhost:8080', - authMetadata: { authProvider: 'serviceAccount' }, + authMetadata: { + [ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'serviceAccount', + }, skipMetricsLookup: false, skipTLSVerify: false, caData: undefined, @@ -235,7 +240,9 @@ describe('ConfigClusterLocator', () => { { name: 'cluster1', url: 'http://localhost:8080', - authMetadata: { authProvider: 'serviceAccount' }, + authMetadata: { + [ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'serviceAccount', + }, skipMetricsLookup: false, skipTLSVerify: false, caData: undefined, @@ -272,7 +279,9 @@ describe('ConfigClusterLocator', () => { { name: 'cluster1', url: 'http://localhost:8080', - authMetadata: { authProvider: 'serviceAccount' }, + authMetadata: { + [ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'serviceAccount', + }, skipMetricsLookup: false, skipTLSVerify: false, caData: undefined, diff --git a/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.ts b/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.ts index 4bf6f44f79..e321d001d3 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.ts @@ -16,6 +16,7 @@ import { Config } from '@backstage/config'; import { + ANNOTATION_KUBERNETES_AUTH_PROVIDER, ANNOTATION_KUBERNETES_AWS_ASSUME_ROLE, ANNOTATION_KUBERNETES_AWS_EXTERNAL_ID, ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER, @@ -45,7 +46,7 @@ export class ConfigClusterLocator implements KubernetesClustersSupplier { caData: c.getOptionalString('caData'), caFile: c.getOptionalString('caFile'), authMetadata: { - authProvider, + [ANNOTATION_KUBERNETES_AUTH_PROVIDER]: authProvider, ...ConfigClusterLocator.parseAuthMetadata(c), }, }; diff --git a/plugins/kubernetes-backend/src/cluster-locator/index.test.ts b/plugins/kubernetes-backend/src/cluster-locator/index.test.ts index 9affb4c588..196de9865d 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/index.test.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/index.test.ts @@ -15,8 +15,9 @@ */ import { Config, ConfigReader } from '@backstage/config'; -import { getCombinedClusterSupplier } from './index'; import { CatalogApi } from '@backstage/catalog-client'; +import { ANNOTATION_KUBERNETES_AUTH_PROVIDER } from '@backstage/plugin-kubernetes-common'; +import { getCombinedClusterSupplier } from './index'; import { ClusterDetails } from '../types/types'; import { AuthenticationStrategy, DispatchStrategy } from '../auth'; @@ -57,12 +58,7 @@ describe('getCombinedClusterSupplier', () => { const clusterSupplier = getCombinedClusterSupplier( config, catalogApi, - new DispatchStrategy({ - authStrategyMap: { - serviceAccount: mockStrategy, - google: mockStrategy, - }, - }), + mockStrategy, ); const result = await clusterSupplier.getClusters(); @@ -71,7 +67,7 @@ describe('getCombinedClusterSupplier', () => { name: 'cluster1', url: 'http://localhost:8080', authMetadata: { - authProvider: 'serviceAccount', + [ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'serviceAccount', serviceAccountToken: 'token', }, skipMetricsLookup: false, @@ -82,7 +78,7 @@ describe('getCombinedClusterSupplier', () => { { name: 'cluster2', url: 'http://localhost:8081', - authMetadata: { authProvider: 'google' }, + authMetadata: { [ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'google' }, skipMetricsLookup: false, skipTLSVerify: false, caData: undefined, diff --git a/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts b/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts index 7c00c1b162..9001c080aa 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts @@ -17,7 +17,11 @@ import { getVoidLogger } from '@backstage/backend-common'; import { Entity } from '@backstage/catalog-model'; import { Config, ConfigReader } from '@backstage/config'; -import { ObjectsByEntityResponse } from '@backstage/plugin-kubernetes-common'; +import { + ANNOTATION_KUBERNETES_AUTH_PROVIDER, + ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER, + ObjectsByEntityResponse, +} from '@backstage/plugin-kubernetes-common'; import express from 'express'; import request from 'supertest'; import { @@ -63,12 +67,17 @@ describe('KubernetesBuilder', () => { { name: 'some-cluster', url: 'https://localhost:1234', - authMetadata: { authProvider: 'serviceAccount' }, + authMetadata: { + [ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'serviceAccount', + }, }, { name: 'some-other-cluster', url: 'https://localhost:1235', - authMetadata: { authProvider: 'google' }, + authMetadata: { + [ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'oidc', + [ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER]: 'google', + }, }, ]; const clusterSupplier: KubernetesClustersSupplier = { @@ -116,7 +125,8 @@ describe('KubernetesBuilder', () => { }, { name: 'some-other-cluster', - authProvider: 'google', + authProvider: 'oidc', + oidcTokenProvider: 'google', }, ], }); @@ -190,14 +200,16 @@ describe('KubernetesBuilder', () => { const someCluster: ClusterDetails = { name: 'some-cluster', url: 'https://localhost:1234', - authMetadata: { authProvider: 'serviceAccount' }, + authMetadata: { + [ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'serviceAccount', + }, }; const clusters: ClusterDetails[] = [ someCluster, { name: 'some-other-cluster', url: 'https://localhost:1235', - authMetadata: { authProvider: 'google' }, + authMetadata: { [ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'google' }, }, ]; const clusterSupplier: KubernetesClustersSupplier = { diff --git a/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts b/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts index e9e631464c..62cc53ed4a 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts @@ -15,7 +15,11 @@ */ import { CatalogApi } from '@backstage/catalog-client'; import { Config } from '@backstage/config'; -import { kubernetesPermissions } from '@backstage/plugin-kubernetes-common'; +import { + ANNOTATION_KUBERNETES_AUTH_PROVIDER, + ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER, + kubernetesPermissions, +} from '@backstage/plugin-kubernetes-common'; import { PermissionEvaluator } from '@backstage/plugin-permission-common'; import { createPermissionIntegrationRouter } from '@backstage/plugin-permission-node'; import express from 'express'; @@ -341,11 +345,12 @@ export class KubernetesBuilder { const clusterDetails = await this.fetchClusterDetails(clusterSupplier); res.json({ items: clusterDetails.map(cd => { - const oidcTokenProvider = cd.authMetadata.oidcTokenProvider; + const oidcTokenProvider = + cd.authMetadata[ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER]; return { name: cd.name, dashboardUrl: cd.dashboardUrl, - authProvider: cd.authMetadata.authProvider, + authProvider: cd.authMetadata[ANNOTATION_KUBERNETES_AUTH_PROVIDER], ...(oidcTokenProvider && { oidcTokenProvider }), }; }), From d21796ce547821ec0c7ebd9deb3fab4c2ee2377e Mon Sep 17 00:00:00 2001 From: Jamie Klassen Date: Thu, 7 Sep 2023 19:08:23 -0400 Subject: [PATCH 14/20] strategies get creds instead of decorating cluster the fetcher now accept these creds, and the fanouthandler passes them along. Signed-off-by: Jamie Klassen --- plugins/kubernetes-backend/api-report.md | 58 ++++---- .../src/auth/AksStrategy.test.ts | 6 +- .../src/auth/AksStrategy.ts | 20 +-- .../src/auth/AwsIamStrategy.test.ts | 21 ++- .../src/auth/AwsIamStrategy.ts | 27 ++-- .../src/auth/AzureIdentityStrategy.test.ts | 54 ++++--- .../src/auth/AzureIdentityStrategy.ts | 24 +--- .../src/auth/DispatchStrategy.test.ts | 38 ++--- .../src/auth/DispatchStrategy.ts | 11 +- .../src/auth/GoogleServiceAccountStrategy.ts | 22 +-- .../src/auth/GoogleStrategy.ts | 26 ++-- .../src/auth/NoopStrategy.ts | 12 +- .../src/auth/OidcStrategy.test.ts | 24 ++-- .../src/auth/OidcStrategy.ts | 22 +-- plugins/kubernetes-backend/src/auth/types.ts | 10 +- .../ConfigClusterLocator.test.ts | 2 +- .../cluster-locator/GkeClusterLocator.test.ts | 17 +-- .../src/cluster-locator/GkeClusterLocator.ts | 3 +- .../LocalKubectlProxyLocator.ts | 5 +- .../src/cluster-locator/index.test.ts | 2 +- .../MultiTenantServiceLocator.test.ts | 26 +--- .../src/service/KubernetesBuilder.test.ts | 2 + .../service/KubernetesFanOutHandler.test.ts | 77 +++++----- .../src/service/KubernetesFanOutHandler.ts | 68 ++++----- .../src/service/KubernetesFetcher.test.ts | 98 +++++-------- .../src/service/KubernetesFetcher.ts | 28 +++- .../src/service/KubernetesProxy.test.ts | 133 +++++------------- .../src/service/KubernetesProxy.ts | 6 +- plugins/kubernetes-backend/src/types/types.ts | 3 + 29 files changed, 330 insertions(+), 515 deletions(-) diff --git a/plugins/kubernetes-backend/api-report.md b/plugins/kubernetes-backend/api-report.md index aee7b16bae..1c5cb766dc 100644 --- a/plugins/kubernetes-backend/api-report.md +++ b/plugins/kubernetes-backend/api-report.md @@ -24,10 +24,10 @@ import { TokenCredential } from '@azure/identity'; // @public (undocumented) export class AksStrategy implements AuthenticationStrategy { // (undocumented) - decorateClusterDetailsWithAuth( - clusterDetails: ClusterDetails, - auth: KubernetesRequestAuth, - ): Promise; + getCredential( + _: ClusterDetails, + requestAuth: KubernetesRequestAuth, + ): Promise; // (undocumented) validate(_: AuthMetadata): void; } @@ -35,10 +35,10 @@ export class AksStrategy implements AuthenticationStrategy { // @public (undocumented) export interface AuthenticationStrategy { // (undocumented) - decorateClusterDetailsWithAuth( + getCredential( clusterDetails: ClusterDetails, authConfig: KubernetesRequestAuth, - ): Promise; + ): Promise; // (undocumented) validate(authMetadata: AuthMetadata): void; } @@ -50,22 +50,18 @@ export type AuthMetadata = Record; export class AwsIamStrategy implements AuthenticationStrategy { constructor(opts: { config: Config }); // (undocumented) - decorateClusterDetailsWithAuth( - clusterDetails: ClusterDetails, - ): Promise; + getCredential(clusterDetails: ClusterDetails): Promise; // (undocumented) - validate(_: AuthMetadata): void; + validate(): void; } // @public (undocumented) export class AzureIdentityStrategy implements AuthenticationStrategy { constructor(logger: Logger, tokenCredential?: TokenCredential); // (undocumented) - decorateClusterDetailsWithAuth( - clusterDetails: ClusterDetails, - ): Promise; + getCredential(): Promise; // (undocumented) - validate(_: AuthMetadata): void; + validate(): void; } // @public (undocumented) @@ -110,10 +106,10 @@ export const DEFAULT_OBJECTS: ObjectToFetch[]; export class DispatchStrategy implements AuthenticationStrategy { constructor(options: DispatchStrategyOptions); // (undocumented) - decorateClusterDetailsWithAuth( + getCredential( clusterDetails: ClusterDetails, auth: KubernetesRequestAuth, - ): Promise; + ): Promise; // (undocumented) validate(authMetadata: AuthMetadata): void; } @@ -136,20 +132,18 @@ export interface FetchResponseWrapper { // @public (undocumented) export class GoogleServiceAccountStrategy implements AuthenticationStrategy { // (undocumented) - decorateClusterDetailsWithAuth( - clusterDetails: ClusterDetails, - ): Promise; + getCredential(): Promise; // (undocumented) - validate(_: AuthMetadata): void; + validate(): void; } // @public (undocumented) export class GoogleStrategy implements AuthenticationStrategy { // (undocumented) - decorateClusterDetailsWithAuth( - clusterDetails: ClusterDetails, - authConfig: KubernetesRequestAuth, - ): Promise; + getCredential( + _: ClusterDetails, + requestAuth: KubernetesRequestAuth, + ): Promise; // (undocumented) validate(_: AuthMetadata): void; } @@ -275,6 +269,9 @@ export interface KubernetesClustersSupplier { getClusters(): Promise; } +// @public +export type KubernetesCredential = string | undefined; + // @public (undocumented) export interface KubernetesEnvironment { // (undocumented) @@ -296,6 +293,7 @@ export interface KubernetesFetcher { // (undocumented) fetchPodMetricsByNamespaces( clusterDetails: ClusterDetails, + credential: KubernetesCredential, namespaces: Set, labelSelector?: string, ): Promise; @@ -388,11 +386,9 @@ export interface KubernetesServiceLocator { // @public (undocumented) export class NoopStrategy implements AuthenticationStrategy { // (undocumented) - decorateClusterDetailsWithAuth( - clusterDetails: ClusterDetails, - ): Promise; + getCredential(): Promise; // (undocumented) - validate(_: AuthMetadata): void; + validate(): void; } // @public (undocumented) @@ -400,6 +396,8 @@ export interface ObjectFetchParams { // (undocumented) clusterDetails: ClusterDetails; // (undocumented) + credential: KubernetesCredential; + // (undocumented) customResources: CustomResource[]; // (undocumented) labelSelector: string; @@ -429,10 +427,10 @@ export interface ObjectToFetch { // @public (undocumented) export class OidcStrategy implements AuthenticationStrategy { // (undocumented) - decorateClusterDetailsWithAuth( + getCredential( clusterDetails: ClusterDetails, authConfig: KubernetesRequestAuth, - ): Promise; + ): Promise; // (undocumented) validate(authMetadata: AuthMetadata): void; } diff --git a/plugins/kubernetes-backend/src/auth/AksStrategy.test.ts b/plugins/kubernetes-backend/src/auth/AksStrategy.test.ts index 6974e302ee..373d053059 100644 --- a/plugins/kubernetes-backend/src/auth/AksStrategy.test.ts +++ b/plugins/kubernetes-backend/src/auth/AksStrategy.test.ts @@ -19,11 +19,11 @@ describe('AksStrategy', () => { it('uses auth.aks value as bearer token', async () => { const strategy = new AksStrategy(); - const details = await strategy.decorateClusterDetailsWithAuth( - { name: '', url: '', authMetadata: { authProvider: 'aks' } }, + const credential = await strategy.getCredential( + { name: '', url: '', authMetadata: {} }, { aks: 'aksToken' }, ); - expect(details.authMetadata.serviceAccountToken).toBe('aksToken'); + expect(credential).toBe('aksToken'); }); }); diff --git a/plugins/kubernetes-backend/src/auth/AksStrategy.ts b/plugins/kubernetes-backend/src/auth/AksStrategy.ts index 0ed2967406..01fe8ffc8e 100644 --- a/plugins/kubernetes-backend/src/auth/AksStrategy.ts +++ b/plugins/kubernetes-backend/src/auth/AksStrategy.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import { AuthMetadata, ClusterDetails } from '../types/types'; -import { AuthenticationStrategy } from './types'; +import { AuthenticationStrategy, KubernetesCredential } from './types'; import { KubernetesRequestAuth } from '@backstage/plugin-kubernetes-common'; /** @@ -22,19 +22,11 @@ import { KubernetesRequestAuth } from '@backstage/plugin-kubernetes-common'; * @public */ export class AksStrategy implements AuthenticationStrategy { - public async decorateClusterDetailsWithAuth( - clusterDetails: ClusterDetails, - auth: KubernetesRequestAuth, - ): Promise { - return { - ...clusterDetails, - ...(auth.aks && { - authMetadata: { - serviceAccountToken: auth.aks, - ...clusterDetails.authMetadata, - }, - }), - }; + public async getCredential( + _: ClusterDetails, + requestAuth: KubernetesRequestAuth, + ): Promise { + return requestAuth.aks; } public validate(_: AuthMetadata) {} } diff --git a/plugins/kubernetes-backend/src/auth/AwsIamStrategy.test.ts b/plugins/kubernetes-backend/src/auth/AwsIamStrategy.test.ts index 0d5fb9a83f..e142717541 100644 --- a/plugins/kubernetes-backend/src/auth/AwsIamStrategy.test.ts +++ b/plugins/kubernetes-backend/src/auth/AwsIamStrategy.test.ts @@ -60,12 +60,12 @@ describe('AwsIamStrategy tests', () => { it('returns a signed url for AWS credentials without assume role', async () => { const strategy = new AwsIamStrategy({ config }); - const authPromise = strategy.decorateClusterDetailsWithAuth({ + const credential = await strategy.getCredential({ name: 'test-cluster', url: '', - authMetadata: { authProvider: 'aws' }, + authMetadata: {}, }); - expect((await authPromise).authMetadata.serviceAccountToken).toEqual( + expect(credential).toEqual( 'k8s-aws-v1.aHR0cHM6Ly9odHRwczovL2V4YW1wbGUuY29tL2FzZGY_', ); }); @@ -73,15 +73,15 @@ describe('AwsIamStrategy tests', () => { it('returns a signed url for AWS credentials with assume role', async () => { const strategy = new AwsIamStrategy({ config }); - const authPromise = strategy.decorateClusterDetailsWithAuth({ + const credential = await strategy.getCredential({ name: 'test-cluster', url: '', authMetadata: { - authProvider: 'aws', [ANNOTATION_KUBERNETES_AWS_ASSUME_ROLE]: 'SomeRole', }, }); - expect((await authPromise).authMetadata.serviceAccountToken).toEqual( + + expect(credential).toEqual( 'k8s-aws-v1.aHR0cHM6Ly9odHRwczovL2V4YW1wbGUuY29tL2FzZGY_', ); expect(fromTemporaryCredentials).toHaveBeenCalledWith({ @@ -101,16 +101,15 @@ describe('AwsIamStrategy tests', () => { it('returns a signed url for AWS credentials and passes the external id', async () => { const strategy = new AwsIamStrategy({ config }); - const authPromise = strategy.decorateClusterDetailsWithAuth({ + const credential = await strategy.getCredential({ name: 'test-cluster', url: '', authMetadata: { - authProvider: 'aws', [ANNOTATION_KUBERNETES_AWS_ASSUME_ROLE]: 'SomeRole', [ANNOTATION_KUBERNETES_AWS_EXTERNAL_ID]: 'external-id', }, }); - expect((await authPromise).authMetadata.serviceAccountToken).toEqual( + expect(credential).toEqual( 'k8s-aws-v1.aHR0cHM6Ly9odHRwczovL2V4YW1wbGUuY29tL2FzZGY_', ); expect(fromTemporaryCredentials).toHaveBeenCalledWith({ @@ -136,10 +135,10 @@ describe('AwsIamStrategy tests', () => { it('throws the right error', async () => { const strategy = new AwsIamStrategy({ config }); await expect( - strategy.decorateClusterDetailsWithAuth({ + strategy.getCredential({ name: 'test-cluster', url: '', - authMetadata: { authProvider: 'aws' }, + authMetadata: {}, }), ).rejects.toThrow('no way'); }); diff --git a/plugins/kubernetes-backend/src/auth/AwsIamStrategy.ts b/plugins/kubernetes-backend/src/auth/AwsIamStrategy.ts index 21a556002b..367df5271a 100644 --- a/plugins/kubernetes-backend/src/auth/AwsIamStrategy.ts +++ b/plugins/kubernetes-backend/src/auth/AwsIamStrategy.ts @@ -25,8 +25,8 @@ import { ANNOTATION_KUBERNETES_AWS_ASSUME_ROLE, ANNOTATION_KUBERNETES_AWS_EXTERNAL_ID, } from '@backstage/plugin-kubernetes-common'; -import { AuthMetadata, ClusterDetails } from '../types/types'; -import { AuthenticationStrategy } from './types'; +import { ClusterDetails } from '../types/types'; +import { AuthenticationStrategy, KubernetesCredential } from './types'; /** * @@ -51,26 +51,17 @@ export class AwsIamStrategy implements AuthenticationStrategy { this.credsManager = DefaultAwsCredentialsManager.fromConfig(opts.config); } - public async decorateClusterDetailsWithAuth( + public getCredential( clusterDetails: ClusterDetails, - ): Promise { - const clusterDetailsWithAuthToken: ClusterDetails = Object.assign( - {}, - clusterDetails, + ): Promise { + return this.getBearerToken( + clusterDetails.name, + clusterDetails.authMetadata[ANNOTATION_KUBERNETES_AWS_ASSUME_ROLE], + clusterDetails.authMetadata[ANNOTATION_KUBERNETES_AWS_EXTERNAL_ID], ); - - clusterDetailsWithAuthToken.authMetadata = { - serviceAccountToken: await this.getBearerToken( - clusterDetails.name, - clusterDetails.authMetadata[ANNOTATION_KUBERNETES_AWS_ASSUME_ROLE], - clusterDetails.authMetadata[ANNOTATION_KUBERNETES_AWS_EXTERNAL_ID], - ), - ...clusterDetailsWithAuthToken.authMetadata, - }; - return clusterDetailsWithAuthToken; } - public validate(_: AuthMetadata) {} + public validate() {} private async getBearerToken( clusterName: string, diff --git a/plugins/kubernetes-backend/src/auth/AzureIdentityStrategy.test.ts b/plugins/kubernetes-backend/src/auth/AzureIdentityStrategy.test.ts index e6aeb9dea1..398bb99516 100644 --- a/plugins/kubernetes-backend/src/auth/AzureIdentityStrategy.test.ts +++ b/plugins/kubernetes-backend/src/auth/AzureIdentityStrategy.test.ts @@ -40,24 +40,18 @@ class StaticTokenCredential implements TokenCredential { } describe('AzureIdentityStrategy tests', () => { - const cd = { - name: 'My Cluster', - url: 'mycluster.privatelink.westeurope.azmk8s.io', - authMetadata: { authProvider: 'azure' }, - }; - afterEach(() => { jest.useRealTimers(); }); - it('should decorate cluster with Azure token', async () => { + it('should get Azure token', async () => { const strategy = new AzureIdentityStrategy( logger, new StaticTokenCredential(5 * 60 * 1000), ); - const response = await strategy.decorateClusterDetailsWithAuth(cd); - expect(response.authMetadata.serviceAccountToken).toEqual('MY_TOKEN_1'); + const credential = await strategy.getCredential(); + expect(credential).toEqual('MY_TOKEN_1'); }); it('should re-use token before expiry', async () => { @@ -66,11 +60,11 @@ describe('AzureIdentityStrategy tests', () => { new StaticTokenCredential(20 * 60 * 1000), ); - const response = await strategy.decorateClusterDetailsWithAuth(cd); - expect(response.authMetadata.serviceAccountToken).toEqual('MY_TOKEN_1'); + const credential = await strategy.getCredential(); + expect(credential).toEqual('MY_TOKEN_1'); - const response2 = await strategy.decorateClusterDetailsWithAuth(cd); - expect(response2.authMetadata.serviceAccountToken).toEqual('MY_TOKEN_1'); + const credential2 = await strategy.getCredential(); + expect(credential2).toEqual('MY_TOKEN_1'); }); it('should issue new token 15 minutes befory expiry', async () => { @@ -81,13 +75,13 @@ describe('AzureIdentityStrategy tests', () => { new StaticTokenCredential(16 * 60 * 1000), // token expires in 16min ); - const response = await strategy.decorateClusterDetailsWithAuth(cd); - expect(response.authMetadata.serviceAccountToken).toEqual('MY_TOKEN_1'); + const credential = await strategy.getCredential(); + expect(credential).toEqual('MY_TOKEN_1'); jest.setSystemTime(Date.now() + 2 * 60 * 1000); // advance time by 2mins - const response2 = await strategy.decorateClusterDetailsWithAuth(cd); - expect(response2.authMetadata.serviceAccountToken).toEqual('MY_TOKEN_2'); + const credential2 = await strategy.getCredential(); + expect(credential2).toEqual('MY_TOKEN_2'); }); it('should re-use existing token if there is afailure', async () => { @@ -98,21 +92,21 @@ describe('AzureIdentityStrategy tests', () => { new StaticTokenCredential(16 * 60 * 1000), // new tokens expires in 16min ); - const response = await strategy.decorateClusterDetailsWithAuth(cd); - expect(response.authMetadata.serviceAccountToken).toEqual('MY_TOKEN_1'); + const credential = await strategy.getCredential(); + expect(credential).toEqual('MY_TOKEN_1'); jest.setSystemTime(Date.now() + 2 * 60 * 1000); // advance time by 2min - const response2 = await strategy.decorateClusterDetailsWithAuth(cd); - expect(response2.authMetadata.serviceAccountToken).toEqual('MY_TOKEN_2'); + const credential2 = await strategy.getCredential(); + expect(credential2).toEqual('MY_TOKEN_2'); jest.setSystemTime(Date.now() + 2 * 60 * 1000); // advance time by 2min - const response3 = await strategy.decorateClusterDetailsWithAuth(cd); - expect(response3.authMetadata.serviceAccountToken).toEqual('MY_TOKEN_2'); + const credential3 = await strategy.getCredential(); + expect(credential3).toEqual('MY_TOKEN_2'); - const response4 = await strategy.decorateClusterDetailsWithAuth(cd); - expect(response4.authMetadata.serviceAccountToken).toEqual('MY_TOKEN_4'); + const credential4 = await strategy.getCredential(); + expect(credential4).toEqual('MY_TOKEN_4'); }); it('should throw if existing token expired and failed to fetch a new one', async () => { @@ -123,16 +117,16 @@ describe('AzureIdentityStrategy tests', () => { new StaticTokenCredential(16 * 60 * 1000), // new tokens expires in 16min ); - const response = await strategy.decorateClusterDetailsWithAuth(cd); - expect(response.authMetadata.serviceAccountToken).toEqual('MY_TOKEN_1'); + const credential = await strategy.getCredential(); + expect(credential).toEqual('MY_TOKEN_1'); jest.setSystemTime(Date.now() + 2 * 60 * 1000); // advance time by 2min - const response2 = await strategy.decorateClusterDetailsWithAuth(cd); - expect(response2.authMetadata.serviceAccountToken).toEqual('MY_TOKEN_2'); + const credential2 = await strategy.getCredential(); + expect(credential2).toEqual('MY_TOKEN_2'); jest.setSystemTime(Date.now() + 17 * 60 * 1000); // advance time by 17min - await expect(strategy.decorateClusterDetailsWithAuth(cd)).rejects.toThrow(); + await expect(strategy.getCredential()).rejects.toThrow(); }); }); diff --git a/plugins/kubernetes-backend/src/auth/AzureIdentityStrategy.ts b/plugins/kubernetes-backend/src/auth/AzureIdentityStrategy.ts index f04af9d37e..f4dbc45612 100644 --- a/plugins/kubernetes-backend/src/auth/AzureIdentityStrategy.ts +++ b/plugins/kubernetes-backend/src/auth/AzureIdentityStrategy.ts @@ -15,8 +15,7 @@ */ import { Logger } from 'winston'; -import { AuthenticationStrategy } from './types'; -import { AuthMetadata, ClusterDetails } from '../types/types'; +import { AuthenticationStrategy, KubernetesCredential } from './types'; import { AccessToken, DefaultAzureCredential, @@ -38,24 +37,7 @@ export class AzureIdentityStrategy implements AuthenticationStrategy { private readonly tokenCredential: TokenCredential = new DefaultAzureCredential(), ) {} - public async decorateClusterDetailsWithAuth( - clusterDetails: ClusterDetails, - ): Promise { - const clusterDetailsWithAuthToken: ClusterDetails = Object.assign( - {}, - clusterDetails, - ); - - clusterDetailsWithAuthToken.authMetadata = { - serviceAccountToken: await this.getToken(), - ...clusterDetailsWithAuthToken.authMetadata, - }; - return clusterDetailsWithAuthToken; - } - - public validate(_: AuthMetadata) {} - - private async getToken(): Promise { + public async getCredential(): Promise { if (!this.tokenRequiresRefresh()) { return this.accessToken.token; } @@ -67,6 +49,8 @@ export class AzureIdentityStrategy implements AuthenticationStrategy { return this.newTokenPromise; } + public validate() {} + private async fetchNewToken(): Promise { try { this.logger.info('Fetching new Azure token for AKS'); diff --git a/plugins/kubernetes-backend/src/auth/DispatchStrategy.test.ts b/plugins/kubernetes-backend/src/auth/DispatchStrategy.test.ts index d791972421..0675b21a9a 100644 --- a/plugins/kubernetes-backend/src/auth/DispatchStrategy.test.ts +++ b/plugins/kubernetes-backend/src/auth/DispatchStrategy.test.ts @@ -22,14 +22,14 @@ import { DispatchStrategy } from './DispatchStrategy'; import { ClusterDetails } from '../types'; import { AuthenticationStrategy } from './types'; -describe('decorateClusterDetailsWithAuth', () => { +describe('getCredential', () => { let strategy: DispatchStrategy; let mockStrategy: jest.Mocked; const authObject: KubernetesRequestAuth = {}; beforeEach(() => { mockStrategy = { - decorateClusterDetailsWithAuth: jest.fn(), + getCredential: jest.fn(), validate: jest.fn(), }; strategy = new DispatchStrategy({ @@ -37,43 +37,29 @@ describe('decorateClusterDetailsWithAuth', () => { }); }); - it('can decorate cluster details if the auth provider is in the strategy map', async () => { - const expectedClusterDetails: ClusterDetails = { + it('gets credential if specified auth provider is in the strategy map', async () => { + const clusterDetails: ClusterDetails = { url: 'notanything.com', name: 'randomName', - authMetadata: { - [ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'google', - serviceAccountToken: 'added by mock strategy', - }, + authMetadata: { [ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'google' }, }; + mockStrategy.getCredential.mockResolvedValue('added by mock strategy'); - mockStrategy.decorateClusterDetailsWithAuth.mockResolvedValue( - expectedClusterDetails, - ); - - const returnedValue = await strategy.decorateClusterDetailsWithAuth( - { - name: 'googleCluster', - url: 'anything.com', - authMetadata: { [ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'google' }, - }, + const returnedValue = await strategy.getCredential( + clusterDetails, authObject, ); - expect(mockStrategy.decorateClusterDetailsWithAuth).toHaveBeenCalledWith( - { - name: 'googleCluster', - url: 'anything.com', - authMetadata: { [ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'google' }, - }, + expect(mockStrategy.getCredential).toHaveBeenCalledWith( + clusterDetails, authObject, ); - expect(returnedValue).toBe(expectedClusterDetails); + expect(returnedValue).toBe('added by mock strategy'); }); it('throws an error when asked for a strategy for an unsupported auth type', () => { expect(() => - strategy.decorateClusterDetailsWithAuth( + strategy.getCredential( { name: 'test-cluster', url: 'anything.com', diff --git a/plugins/kubernetes-backend/src/auth/DispatchStrategy.ts b/plugins/kubernetes-backend/src/auth/DispatchStrategy.ts index f4150ad8b6..b16340f8bc 100644 --- a/plugins/kubernetes-backend/src/auth/DispatchStrategy.ts +++ b/plugins/kubernetes-backend/src/auth/DispatchStrategy.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { AuthenticationStrategy } from './types'; +import { AuthenticationStrategy, KubernetesCredential } from './types'; import { AuthMetadata, ClusterDetails } from '../types'; import { ANNOTATION_KUBERNETES_AUTH_PROVIDER, @@ -41,17 +41,14 @@ export class DispatchStrategy implements AuthenticationStrategy { this.strategyMap = options.authStrategyMap; } - public decorateClusterDetailsWithAuth( + public getCredential( clusterDetails: ClusterDetails, auth: KubernetesRequestAuth, - ) { + ): Promise { const authProvider = clusterDetails.authMetadata[ANNOTATION_KUBERNETES_AUTH_PROVIDER]; if (this.strategyMap[authProvider]) { - return this.strategyMap[authProvider].decorateClusterDetailsWithAuth( - clusterDetails, - auth, - ); + return this.strategyMap[authProvider].getCredential(clusterDetails, auth); } throw new Error( `authProvider "${authProvider}" has no AuthenticationStrategy associated with it`, diff --git a/plugins/kubernetes-backend/src/auth/GoogleServiceAccountStrategy.ts b/plugins/kubernetes-backend/src/auth/GoogleServiceAccountStrategy.ts index dcb230eaf7..66e0286372 100644 --- a/plugins/kubernetes-backend/src/auth/GoogleServiceAccountStrategy.ts +++ b/plugins/kubernetes-backend/src/auth/GoogleServiceAccountStrategy.ts @@ -13,8 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { AuthenticationStrategy } from './types'; -import { AuthMetadata, ClusterDetails } from '../types/types'; +import { AuthenticationStrategy, KubernetesCredential } from './types'; import * as container from '@google-cloud/container'; /** @@ -22,28 +21,17 @@ import * as container from '@google-cloud/container'; * @public */ export class GoogleServiceAccountStrategy implements AuthenticationStrategy { - public async decorateClusterDetailsWithAuth( - clusterDetails: ClusterDetails, - ): Promise { - const clusterDetailsWithAuthToken: ClusterDetails = Object.assign( - {}, - clusterDetails, - ); + public async getCredential(): Promise { const client = new container.v1.ClusterManagerClient(); const accessToken = await client.auth.getAccessToken(); - if (accessToken) { - clusterDetailsWithAuthToken.authMetadata = { - serviceAccountToken: accessToken, - ...clusterDetailsWithAuthToken.authMetadata, - }; - } else { + if (!accessToken) { throw new Error( 'Unable to obtain access token for the current Google Application Default Credentials', ); } - return clusterDetailsWithAuthToken; + return accessToken; } - public validate(_: AuthMetadata) {} + public validate() {} } diff --git a/plugins/kubernetes-backend/src/auth/GoogleStrategy.ts b/plugins/kubernetes-backend/src/auth/GoogleStrategy.ts index 79d9df2059..3111e02651 100644 --- a/plugins/kubernetes-backend/src/auth/GoogleStrategy.ts +++ b/plugins/kubernetes-backend/src/auth/GoogleStrategy.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { AuthenticationStrategy } from './types'; +import { AuthenticationStrategy, KubernetesCredential } from './types'; import { AuthMetadata, ClusterDetails } from '../types/types'; import { KubernetesRequestAuth } from '@backstage/plugin-kubernetes-common'; @@ -23,27 +23,17 @@ import { KubernetesRequestAuth } from '@backstage/plugin-kubernetes-common'; * @public */ export class GoogleStrategy implements AuthenticationStrategy { - public async decorateClusterDetailsWithAuth( - clusterDetails: ClusterDetails, - authConfig: KubernetesRequestAuth, - ): Promise { - const clusterDetailsWithAuthToken: ClusterDetails = Object.assign( - {}, - clusterDetails, - ); - const authToken: string | undefined = authConfig.google; - - if (authToken) { - clusterDetailsWithAuthToken.authMetadata = { - serviceAccountToken: authToken, - ...clusterDetailsWithAuthToken.authMetadata, - }; - } else { + public async getCredential( + _: ClusterDetails, + requestAuth: KubernetesRequestAuth, + ): Promise { + const authToken = requestAuth.google; + if (!authToken) { throw new Error( 'Google token not found under auth.google in request body', ); } - return clusterDetailsWithAuthToken; + return authToken; } public validate(_: AuthMetadata) {} } diff --git a/plugins/kubernetes-backend/src/auth/NoopStrategy.ts b/plugins/kubernetes-backend/src/auth/NoopStrategy.ts index 80841e2a19..1af8f09cf8 100644 --- a/plugins/kubernetes-backend/src/auth/NoopStrategy.ts +++ b/plugins/kubernetes-backend/src/auth/NoopStrategy.ts @@ -14,19 +14,19 @@ * limitations under the License. */ -import { AuthenticationStrategy } from './types'; -import { AuthMetadata, ClusterDetails } from '../types/types'; +import { AuthenticationStrategy, KubernetesCredential } from './types'; +import { ClusterDetails } from '../types/types'; /** * * @public */ export class NoopStrategy implements AuthenticationStrategy { - public async decorateClusterDetailsWithAuth( + public async getCredential( clusterDetails: ClusterDetails, - ): Promise { - return clusterDetails; + ): Promise { + return clusterDetails.authMetadata.serviceAccountToken; } - public validate(_: AuthMetadata) {} + public validate() {} } diff --git a/plugins/kubernetes-backend/src/auth/OidcStrategy.test.ts b/plugins/kubernetes-backend/src/auth/OidcStrategy.test.ts index 7843cc6544..0502f91fad 100644 --- a/plugins/kubernetes-backend/src/auth/OidcStrategy.test.ts +++ b/plugins/kubernetes-backend/src/auth/OidcStrategy.test.ts @@ -15,7 +15,6 @@ */ import { ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER } from '@backstage/plugin-kubernetes-common'; -import { AuthMetadata } from '../types/types'; import { OidcStrategy } from './OidcStrategy'; describe('OidcStrategy', () => { @@ -24,14 +23,13 @@ describe('OidcStrategy', () => { strategy = new OidcStrategy(); }); - describe('decorateClusterDetailsWithAuth', () => { - it('returns cluster details with auth token', async () => { - const details = await strategy.decorateClusterDetailsWithAuth( + describe('getCredential', () => { + it('returns auth token', async () => { + const credential = await strategy.getCredential( { name: 'test', url: '', authMetadata: { - authProvider: 'oidc', [ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER]: 'okta', }, }, @@ -40,16 +38,16 @@ describe('OidcStrategy', () => { }, ); - expect(details.authMetadata.serviceAccountToken).toBe('fakeToken'); + expect(credential).toBe('fakeToken'); }); - it('fails when oidcTokenProvider is not configured', async () => { + it('fails when token provider is not configured', async () => { await expect( - strategy.decorateClusterDetailsWithAuth( + strategy.getCredential( { name: 'test', url: '', - authMetadata: { authProvider: 'oidc' }, + authMetadata: {}, }, {}, ), @@ -60,12 +58,11 @@ describe('OidcStrategy', () => { it('fails when token is not included in request body', async () => { await expect( - strategy.decorateClusterDetailsWithAuth( + strategy.getCredential( { name: 'test', url: '', authMetadata: { - authProvider: 'oidc', [ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER]: 'okta', }, }, @@ -77,10 +74,7 @@ describe('OidcStrategy', () => { describe('validate', () => { it('fails when token provider is not specified', () => { - const authMetadata: AuthMetadata = { - authProvider: 'oidc', - }; - expect(() => strategy.validate(authMetadata)).toThrow( + expect(() => strategy.validate({})).toThrow( `Must specify a token provider for 'oidc' strategy`, ); }); diff --git a/plugins/kubernetes-backend/src/auth/OidcStrategy.ts b/plugins/kubernetes-backend/src/auth/OidcStrategy.ts index 083c28dd1b..21410686de 100644 --- a/plugins/kubernetes-backend/src/auth/OidcStrategy.ts +++ b/plugins/kubernetes-backend/src/auth/OidcStrategy.ts @@ -17,7 +17,7 @@ import { ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER, KubernetesRequestAuth, } from '@backstage/plugin-kubernetes-common'; -import { AuthenticationStrategy } from './types'; +import { AuthenticationStrategy, KubernetesCredential } from './types'; import { AuthMetadata, ClusterDetails } from '../types/types'; /** @@ -25,15 +25,10 @@ import { AuthMetadata, ClusterDetails } from '../types/types'; * @public */ export class OidcStrategy implements AuthenticationStrategy { - public async decorateClusterDetailsWithAuth( + public async getCredential( clusterDetails: ClusterDetails, authConfig: KubernetesRequestAuth, - ): Promise { - const clusterDetailsWithAuthToken: ClusterDetails = Object.assign( - {}, - clusterDetails, - ); - + ): Promise { const oidcTokenProvider = clusterDetails.authMetadata[ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER]; @@ -43,19 +38,14 @@ export class OidcStrategy implements AuthenticationStrategy { ); } - const authToken: string | undefined = authConfig.oidc?.[oidcTokenProvider]; + const authToken = authConfig.oidc?.[oidcTokenProvider]; - if (authToken) { - clusterDetailsWithAuthToken.authMetadata = { - serviceAccountToken: authToken, - ...clusterDetailsWithAuthToken.authMetadata, - }; - } else { + if (!authToken) { throw new Error( `Auth token not found under oidc.${oidcTokenProvider} in request body`, ); } - return clusterDetailsWithAuthToken; + return authToken; } public validate(authMetadata: AuthMetadata) { diff --git a/plugins/kubernetes-backend/src/auth/types.ts b/plugins/kubernetes-backend/src/auth/types.ts index ed57ecc1f6..e9a6ce3a16 100644 --- a/plugins/kubernetes-backend/src/auth/types.ts +++ b/plugins/kubernetes-backend/src/auth/types.ts @@ -17,14 +17,20 @@ import { AuthMetadata, ClusterDetails } from '../types/types'; import { KubernetesRequestAuth } from '@backstage/plugin-kubernetes-common'; +/** + * Authentication data used to make a request to Kubernetes + * @public + */ +export type KubernetesCredential = string | undefined; + /** * * @public */ export interface AuthenticationStrategy { - decorateClusterDetailsWithAuth( + getCredential( clusterDetails: ClusterDetails, authConfig: KubernetesRequestAuth, - ): Promise; + ): Promise; validate(authMetadata: AuthMetadata): void; } diff --git a/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.test.ts b/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.test.ts index 73c81e0f59..e48752bbd0 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.test.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.test.ts @@ -30,7 +30,7 @@ describe('ConfigClusterLocator', () => { beforeEach(() => { authStrategy = { - decorateClusterDetailsWithAuth: jest.fn(), + getCredential: jest.fn(), validate: jest.fn(), }; }); diff --git a/plugins/kubernetes-backend/src/cluster-locator/GkeClusterLocator.test.ts b/plugins/kubernetes-backend/src/cluster-locator/GkeClusterLocator.test.ts index fef1e8a3ca..f919561490 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/GkeClusterLocator.test.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/GkeClusterLocator.test.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { ANNOTATION_KUBERNETES_AUTH_PROVIDER } from '@backstage/plugin-kubernetes-common'; import '@backstage/backend-common'; import { ConfigReader, Config } from '@backstage/config'; import { GkeClusterLocator } from './GkeClusterLocator'; @@ -106,7 +107,7 @@ describe('GkeClusterLocator', () => { { name: 'some-cluster', url: 'https://1.2.3.4', - authMetadata: { authProvider: 'google' }, + authMetadata: { [ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'google' }, skipTLSVerify: false, skipMetricsLookup: true, }, @@ -143,7 +144,7 @@ describe('GkeClusterLocator', () => { { name: 'some-cluster', url: 'https://1.2.3.4', - authMetadata: { authProvider: 'google' }, + authMetadata: { [ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'google' }, skipTLSVerify: false, skipMetricsLookup: false, }, @@ -185,14 +186,14 @@ describe('GkeClusterLocator', () => { { name: 'some-cluster', url: 'https://1.2.3.4', - authMetadata: { authProvider: 'google' }, + authMetadata: { [ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'google' }, skipTLSVerify: false, skipMetricsLookup: false, }, { name: 'some-other-cluster', url: 'https://6.7.8.9', - authMetadata: { authProvider: 'google' }, + authMetadata: { [ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'google' }, skipTLSVerify: false, skipMetricsLookup: false, }, @@ -240,14 +241,14 @@ describe('GkeClusterLocator', () => { { name: 'some-cluster', url: 'https://1.2.3.4', - authMetadata: { authProvider: 'google' }, + authMetadata: { [ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'google' }, skipTLSVerify: false, skipMetricsLookup: false, }, { name: 'some-other-cluster', url: 'https://6.7.8.9', - authMetadata: { authProvider: 'google' }, + authMetadata: { [ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'google' }, skipTLSVerify: false, skipMetricsLookup: false, }, @@ -301,7 +302,7 @@ describe('GkeClusterLocator', () => { { name: 'some-cluster', url: 'https://1.2.3.4', - authMetadata: { authProvider: 'google' }, + authMetadata: { [ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'google' }, skipTLSVerify: false, skipMetricsLookup: false, }, @@ -365,7 +366,7 @@ describe('GkeClusterLocator', () => { { name: 'some-cluster', url: 'https://1.2.3.4', - authMetadata: { authProvider: 'google' }, + authMetadata: { [ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'google' }, skipTLSVerify: false, skipMetricsLookup: true, dashboardApp: 'gke', diff --git a/plugins/kubernetes-backend/src/cluster-locator/GkeClusterLocator.ts b/plugins/kubernetes-backend/src/cluster-locator/GkeClusterLocator.ts index 0e45898639..74e7ebc9af 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/GkeClusterLocator.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/GkeClusterLocator.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { ANNOTATION_KUBERNETES_AUTH_PROVIDER } from '@backstage/plugin-kubernetes-common'; import { Config } from '@backstage/config'; import { ForwardedError } from '@backstage/errors'; import * as container from '@google-cloud/container'; @@ -120,7 +121,7 @@ export class GkeClusterLocator implements KubernetesClustersSupplier { // TODO filter out clusters which don't have name or endpoint name: r.name ?? 'unknown', url: `https://${r.endpoint ?? ''}`, - authMetadata: { authProvider: 'google' }, + authMetadata: { [ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'google' }, skipTLSVerify, skipMetricsLookup, ...(exposeDashboard diff --git a/plugins/kubernetes-backend/src/cluster-locator/LocalKubectlProxyLocator.ts b/plugins/kubernetes-backend/src/cluster-locator/LocalKubectlProxyLocator.ts index d393087da2..9bbab42443 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/LocalKubectlProxyLocator.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/LocalKubectlProxyLocator.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { ANNOTATION_KUBERNETES_AUTH_PROVIDER } from '@backstage/plugin-kubernetes-common'; import { ClusterDetails, KubernetesClustersSupplier } from '../types/types'; export class LocalKubectlProxyClusterLocator @@ -26,7 +27,9 @@ export class LocalKubectlProxyClusterLocator { name: 'local', url: 'http:/localhost:8001', - authMetadata: { authProvider: 'localKubectlProxy' }, + authMetadata: { + [ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'localKubectlProxy', + }, skipMetricsLookup: true, }, ]; diff --git a/plugins/kubernetes-backend/src/cluster-locator/index.test.ts b/plugins/kubernetes-backend/src/cluster-locator/index.test.ts index 196de9865d..e7a58b3f98 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/index.test.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/index.test.ts @@ -51,7 +51,7 @@ describe('getCombinedClusterSupplier', () => { 'ctx', ); const mockStrategy: jest.Mocked = { - decorateClusterDetailsWithAuth: jest.fn(), + getCredential: jest.fn(), validate: jest.fn(), }; diff --git a/plugins/kubernetes-backend/src/service-locator/MultiTenantServiceLocator.test.ts b/plugins/kubernetes-backend/src/service-locator/MultiTenantServiceLocator.test.ts index 18cc72ffb1..093831164e 100644 --- a/plugins/kubernetes-backend/src/service-locator/MultiTenantServiceLocator.test.ts +++ b/plugins/kubernetes-backend/src/service-locator/MultiTenantServiceLocator.test.ts @@ -16,7 +16,7 @@ import '@backstage/backend-common'; import { Entity } from '@backstage/catalog-model'; -import { AuthMetadata, ServiceLocatorRequestContext } from '../types/types'; +import { ServiceLocatorRequestContext } from '../types/types'; import { MultiTenantServiceLocator } from './MultiTenantServiceLocator'; describe('MultiTenantConfigClusterLocator', () => { @@ -40,10 +40,7 @@ describe('MultiTenantConfigClusterLocator', () => { { name: 'cluster1', url: 'http://localhost:8080', - authMetadata: { - authProvider: 'serviceAccount', - serviceAccountToken: '12345', - }, + authMetadata: {}, }, ]; }, @@ -59,10 +56,7 @@ describe('MultiTenantConfigClusterLocator', () => { { name: 'cluster1', url: 'http://localhost:8080', - authMetadata: { - authProvider: 'serviceAccount', - serviceAccountToken: '12345', - }, + authMetadata: {}, }, ], }); @@ -75,15 +69,12 @@ describe('MultiTenantConfigClusterLocator', () => { { name: 'cluster1', url: 'http://localhost:8080', - authMetadata: { - authProvider: 'serviceAccount', - serviceAccountToken: 'token', - } as AuthMetadata, + authMetadata: {}, }, { name: 'cluster2', url: 'http://localhost:8081', - authMetadata: { authProvider: 'google' } as AuthMetadata, + authMetadata: {}, }, ]; }, @@ -99,15 +90,12 @@ describe('MultiTenantConfigClusterLocator', () => { { name: 'cluster1', url: 'http://localhost:8080', - authMetadata: { - authProvider: 'serviceAccount', - serviceAccountToken: 'token', - }, + authMetadata: {}, }, { name: 'cluster2', url: 'http://localhost:8081', - authMetadata: { authProvider: 'google' }, + authMetadata: {}, }, ], }); diff --git a/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts b/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts index 9001c080aa..a428c7ca0e 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts @@ -32,6 +32,7 @@ import { KubernetesServiceLocator, ObjectFetchParams, } from '../types/types'; +import { KubernetesCredential } from '../auth/types'; import { KubernetesBuilder } from './KubernetesBuilder'; import { KubernetesFanOutHandler } from './KubernetesFanOutHandler'; import { CatalogApi } from '@backstage/catalog-client'; @@ -251,6 +252,7 @@ describe('KubernetesBuilder', () => { const fetcher: KubernetesFetcher = { fetchPodMetricsByNamespaces( _clusterDetails: ClusterDetails, + _credential: KubernetesCredential, _namespaces: Set, ): Promise { return Promise.resolve({ errors: [], responses: [] }); diff --git a/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.test.ts b/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.test.ts index 8fc22946d4..35c8ead904 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.test.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.test.ts @@ -22,6 +22,7 @@ import { KubernetesServiceLocator, ServiceLocatorRequestContext, } from '../types/types'; +import { KubernetesCredential } from '../auth/types'; import { KubernetesFanOutHandler } from './KubernetesFanOutHandler'; import { KubernetesClientBasedFetcher } from './KubernetesFetcher'; import { rest } from 'msw'; @@ -77,7 +78,7 @@ describe('KubernetesFanOutHandler', () => { const cluster1 = { name: 'test-cluster', url: '', - authMetadata: { authProvider: 'serviceAccount' }, + authMetadata: {}, customResources: [ { group: 'some-other-crd.example.com', @@ -90,7 +91,7 @@ describe('KubernetesFanOutHandler', () => { const cluster2 = { name: 'cluster-two', url: '', - authMetadata: { authProvider: 'serviceAccount' }, + authMetadata: {}, customResources: [ { group: 'crd-two.example.com', @@ -185,9 +186,7 @@ describe('KubernetesFanOutHandler', () => { }, customResources: customResources, authStrategy: { - decorateClusterDetailsWithAuth: async (clusterDetails, _) => { - return clusterDetails; - }, + getCredential: jest.fn().mockResolvedValue(undefined), validate: jest.fn(), }, config, @@ -316,7 +315,11 @@ describe('KubernetesFanOutHandler', () => { ); fetchPodMetricsByNamespaces.mockImplementation( - (_clusterDetails: ClusterDetails, namespaces: Set) => + ( + _clusterDetails: ClusterDetails, + _: KubernetesCredential, + namespaces: Set, + ) => Promise.resolve({ errors: [], responses: Array.from(namespaces).map(() => { @@ -352,7 +355,7 @@ describe('KubernetesFanOutHandler', () => { { name: 'test-cluster', url: '', - authMetadata: { authProvider: 'serviceAccount' }, + authMetadata: {}, }, ], }), @@ -370,6 +373,7 @@ describe('KubernetesFanOutHandler', () => { expect(fetchPodMetricsByNamespaces).toHaveBeenCalledTimes(1); expect(fetchPodMetricsByNamespaces).toHaveBeenCalledWith( expect.anything(), + undefined, new Set(['ns-test-component-test-cluster']), expect.anything(), ); @@ -467,7 +471,7 @@ describe('KubernetesFanOutHandler', () => { { name: 'test-cluster', url: '', - authMetadata: { authProvider: 'serviceAccount' }, + authMetadata: {}, }, cluster2, ], @@ -522,7 +526,7 @@ describe('KubernetesFanOutHandler', () => { { name: 'profile-cluster-1', url: '', - authMetadata: { authProvider: 'serviceAccount' }, + authMetadata: {}, customResourceProfile: 'build', customResources: [ { @@ -571,7 +575,7 @@ describe('KubernetesFanOutHandler', () => { { name: 'profile-cluster-1', url: '', - authMetadata: { authProvider: 'serviceAccount' }, + authMetadata: {}, }, ], }), @@ -612,7 +616,7 @@ describe('KubernetesFanOutHandler', () => { { name: 'test-cluster', url: '', - authMetadata: { authProvider: 'serviceAccount' }, + authMetadata: {}, }, ], }), @@ -661,6 +665,7 @@ describe('KubernetesFanOutHandler', () => { expect(fetchPodMetricsByNamespaces).toHaveBeenCalledTimes(1); expect(fetchPodMetricsByNamespaces).toHaveBeenCalledWith( expect.anything(), + undefined, new Set(['ns-a', 'ns-b']), expect.anything(), ); @@ -710,7 +715,7 @@ describe('KubernetesFanOutHandler', () => { { name: 'test-cluster', url: '', - authMetadata: { authProvider: 'serviceAccount' }, + authMetadata: {}, }, ], }), @@ -750,13 +755,13 @@ describe('KubernetesFanOutHandler', () => { { name: 'test-cluster', url: '', - authMetadata: { authProvider: 'serviceAccount' }, + authMetadata: {}, dashboardUrl: 'https://k8s.foo.coom', }, { name: 'other-cluster', url: '', - authMetadata: { authProvider: 'google' }, + authMetadata: {}, }, ], }), @@ -803,17 +808,17 @@ describe('KubernetesFanOutHandler', () => { { name: 'test-cluster', url: '', - authMetadata: { authProvider: 'serviceAccount' }, + authMetadata: {}, }, { name: 'other-cluster', url: '', - authMetadata: { authProvider: 'google' }, + authMetadata: {}, }, { name: 'empty-cluster', url: '', - authMetadata: { authProvider: 'google' }, + authMetadata: {}, }, ], }), @@ -859,22 +864,22 @@ describe('KubernetesFanOutHandler', () => { { name: 'test-cluster', url: '', - authMetadata: { authProvider: 'serviceAccount' }, + authMetadata: {}, }, { name: 'other-cluster', url: '', - authMetadata: { authProvider: 'google' }, + authMetadata: {}, }, { name: 'empty-cluster', url: '', - authMetadata: { authProvider: 'google' }, + authMetadata: {}, }, { name: 'error-cluster', url: '', - authMetadata: { authProvider: 'google' }, + authMetadata: {}, }, ], }), @@ -941,13 +946,13 @@ describe('KubernetesFanOutHandler', () => { { name: 'test-cluster', url: '', - authMetadata: { authProvider: 'serviceAccount' }, + authMetadata: {}, dashboardUrl: 'https://k8s.foo.coom', }, { name: 'other-cluster', url: '', - authMetadata: { authProvider: 'google' }, + authMetadata: {}, }, ], }), @@ -957,7 +962,11 @@ describe('KubernetesFanOutHandler', () => { // and an error for the second call. fetchPodMetricsByNamespaces .mockImplementationOnce( - (_clusterDetails: ClusterDetails, namespaces: Set) => + ( + _clusterDetails: ClusterDetails, + _: KubernetesCredential, + namespaces: Set, + ) => Promise.resolve({ errors: [], responses: Array.from(namespaces).map(() => { @@ -1041,7 +1050,7 @@ describe('KubernetesFanOutHandler', () => { { name: 'test-cluster', url: '', - authMetadata: { authProvider: 'serviceAccount' }, + authMetadata: {}, skipMetricsLookup: true, }, ], @@ -1090,19 +1099,13 @@ describe('KubernetesFanOutHandler', () => { { name: 'works', url: 'https://works', - authMetadata: { - authProvider: 'serviceAccount', - serviceAccountToken: 'token', - }, + authMetadata: {}, skipMetricsLookup: true, }, { name: 'fails', url: 'https://fails', - authMetadata: { - authProvider: 'serviceAccount', - serviceAccountToken: 'token', - }, + authMetadata: {}, skipMetricsLookup: true, }, ], @@ -1129,9 +1132,7 @@ describe('KubernetesFanOutHandler', () => { }, ], authStrategy: { - decorateClusterDetailsWithAuth: async (clusterDetails, _) => { - return clusterDetails; - }, + getCredential: jest.fn().mockResolvedValue('token'), validate: jest.fn(), }, config, @@ -1229,7 +1230,7 @@ describe('KubernetesFanOutHandler', () => { { name: 'test-cluster', url: '', - authMetadata: { authProvider: 'serviceAccount' }, + authMetadata: {}, }, cluster2, ], @@ -1303,7 +1304,7 @@ describe('KubernetesFanOutHandler', () => { { name: 'profile-cluster-1', url: '', - authMetadata: { authProvider: 'serviceAccount' }, + authMetadata: {}, customResourceProfile: 'build', }, ], diff --git a/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.ts b/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.ts index 98ad41947b..f2f1c6d38f 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.ts @@ -27,9 +27,8 @@ import { CustomResource, CustomResourcesByEntity, KubernetesObjectsByEntity, - ServiceLocatorRequestContext, } from '../types/types'; -import { AuthenticationStrategy } from '../auth/types'; +import { AuthenticationStrategy, KubernetesCredential } from '../auth/types'; import { ClientContainerStatus, ClientCurrentResourceUsage, @@ -48,14 +47,6 @@ import { PodStatus, } from '@kubernetes/client-node'; -const isRejected = ( - input: PromiseSettledResult, -): input is PromiseRejectedResult => input.status === 'rejected'; - -const isFulfilled = ( - input: PromiseSettledResult, -): input is PromiseFulfilledResult => input.status === 'fulfilled'; - /** * * @public @@ -245,14 +236,13 @@ export class KubernetesFanOutHandler { entity.metadata?.annotations?.['backstage.io/kubernetes-id'] || entity.metadata?.name; - const clusterDetailsDecoratedForAuth: ClusterDetails[] = - await this.decorateClusterDetailsWithAuth(entity, auth, { - objectTypesToFetch: objectTypesToFetch, - customResources: customResources ?? [], - }); + const { clusters } = await this.serviceLocator.getClustersByEntity(entity, { + objectTypesToFetch: objectTypesToFetch, + customResources: customResources ?? [], + }); this.logger.info( - `entity.metadata.name=${entityName} clusterDetails=[${clusterDetailsDecoratedForAuth + `entity.metadata.name=${entityName} clusterDetails=[${clusters .map(c => c.name) .join(', ')}]`, ); @@ -266,16 +256,21 @@ export class KubernetesFanOutHandler { entity.metadata?.annotations?.['backstage.io/kubernetes-namespace']; return Promise.all( - clusterDetailsDecoratedForAuth.map(clusterDetailsItem => { + clusters.map(async clusterDetails => { + const credential = await this.authStrategy.getCredential( + clusterDetails, + auth, + ); return this.fetcher .fetchObjectsForService({ serviceId: entityName, - clusterDetails: clusterDetailsItem, - objectTypesToFetch: objectTypesToFetch, + clusterDetails, + credential, + objectTypesToFetch, labelSelector, customResources: ( customResources || - clusterDetailsItem.customResources || + clusterDetails.customResources || this.customResources ).map(c => ({ ...c, @@ -284,7 +279,12 @@ export class KubernetesFanOutHandler { namespace, }) .then(result => - this.getMetricsForPods(clusterDetailsItem, labelSelector, result), + this.getMetricsForPods( + clusterDetails, + credential, + labelSelector, + result, + ), ) .catch( (e): Promise => @@ -300,33 +300,11 @@ export class KubernetesFanOutHandler { ]) : Promise.reject(e), ) - .then(r => this.toClusterObjects(clusterDetailsItem, r)); + .then(r => this.toClusterObjects(clusterDetails, r)); }), ).then(this.toObjectsByEntityResponse); } - private async decorateClusterDetailsWithAuth( - entity: Entity, - auth: KubernetesRequestAuth, - requestContext: ServiceLocatorRequestContext, - ) { - const clusterDetails: ClusterDetails[] = ( - await this.serviceLocator.getClustersByEntity(entity, requestContext) - ).clusters; - - // Execute all of these async actions simultaneously/without blocking sequentially as no common object is modified by them - const promiseResults = await Promise.allSettled( - clusterDetails.map(cd => { - return this.authStrategy.decorateClusterDetailsWithAuth(cd, auth); - }), - ); - - promiseResults.filter(isRejected).map(item => { - this.logger.info(`Failed to decorate cluster details: ${item.reason}`); - }); - return promiseResults.filter(isFulfilled).map(item => item.value); - } - toObjectsByEntityResponse( clusterObjects: ClusterObjects[], ): ObjectsByEntityResponse { @@ -367,6 +345,7 @@ export class KubernetesFanOutHandler { async getMetricsForPods( clusterDetails: ClusterDetails, + credential: KubernetesCredential, labelSelector: string, result: FetchResponseWrapper, ): Promise { @@ -387,6 +366,7 @@ export class KubernetesFanOutHandler { const podMetrics = await this.fetcher.fetchPodMetricsByNamespaces( clusterDetails, + credential, namespaces, labelSelector, ); diff --git a/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts b/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts index 0c47c59184..6feac880ec 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { ANNOTATION_KUBERNETES_AUTH_PROVIDER } from '@backstage/plugin-kubernetes-common'; import { getVoidLogger } from '@backstage/backend-common'; import { KubernetesClientBasedFetcher } from './KubernetesFetcher'; import { ObjectToFetch } from '../types/types'; @@ -138,11 +139,9 @@ describe('KubernetesFetcher', () => { clusterDetails: { name: 'cluster1', url: 'http://localhost:9999', - authMetadata: { - authProvider: 'serviceAccount', - serviceAccountToken: 'token', - }, + authMetadata: {}, }, + credential: 'token', objectTypesToFetch: OBJECTS_TO_FETCH, labelSelector: '', customResources: [], @@ -201,11 +200,9 @@ describe('KubernetesFetcher', () => { clusterDetails: { name: 'cluster1', url: 'http://localhost:9999/k8s/clusters/1234', - authMetadata: { - authProvider: 'serviceAccount', - serviceAccountToken: 'token', - }, + authMetadata: {}, }, + credential: 'token', objectTypesToFetch: OBJECTS_TO_FETCH, labelSelector: '', customResources: [], @@ -257,8 +254,11 @@ describe('KubernetesFetcher', () => { clusterDetails: { name: 'cluster1', url: 'http://localhost:9999/k8s/clusters/1234', - authMetadata: { authProvider: 'localKubectlProxy' }, + authMetadata: { + [ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'localKubectlProxy', + }, }, + credential: undefined, objectTypesToFetch: new Set([ { group: '', @@ -313,11 +313,9 @@ describe('KubernetesFetcher', () => { clusterDetails: { name: 'cluster1', url: 'http://localhost:9999', - authMetadata: { - authProvider: 'serviceAccount', - serviceAccountToken: 'token', - }, + authMetadata: {}, }, + credential: 'token', objectTypesToFetch: OBJECTS_TO_FETCH, labelSelector: '', customResources: [], @@ -389,11 +387,9 @@ describe('KubernetesFetcher', () => { clusterDetails: { name: 'cluster1', url: 'http://localhost:9999', - authMetadata: { - authProvider: 'serviceAccount', - serviceAccountToken: 'token', - }, + authMetadata: {}, }, + credential: 'token', objectTypesToFetch: OBJECTS_TO_FETCH, labelSelector: '', customResources: [ @@ -483,11 +479,9 @@ describe('KubernetesFetcher', () => { clusterDetails: { name: 'cluster1', url: 'http://localhost:9999', - authMetadata: { - authProvider: 'serviceAccount', - serviceAccountToken: 'token', - }, + authMetadata: {}, }, + credential: 'token', objectTypesToFetch: OBJECTS_TO_FETCH, labelSelector: '', customResources: [], @@ -573,11 +567,9 @@ describe('KubernetesFetcher', () => { clusterDetails: { name: 'cluster1', url: 'http://badurl.does.not.exist', - authMetadata: { - authProvider: 'serviceAccount', - serviceAccountToken: 'token', - }, + authMetadata: {}, }, + credential: 'token', objectTypesToFetch: OBJECTS_TO_FETCH, labelSelector: '', customResources: [], @@ -612,11 +604,9 @@ describe('KubernetesFetcher', () => { clusterDetails: { name: 'cluster1', url: 'http://localhost:9999', - authMetadata: { - authProvider: 'serviceAccount', - serviceAccountToken: 'token', - }, + authMetadata: {}, }, + credential: 'token', objectTypesToFetch: OBJECTS_TO_FETCH, labelSelector: 'service-label=value', customResources: [], @@ -681,12 +671,10 @@ describe('KubernetesFetcher', () => { clusterDetails: { name: 'cluster1', url: 'https://localhost:9999', - authMetadata: { - authProvider: 'serviceAccount', - serviceAccountToken: 'token', - }, + authMetadata: {}, caData: 'MOCKCA', }, + credential: 'token', objectTypesToFetch: new Set([ { group: '', @@ -720,11 +708,9 @@ describe('KubernetesFetcher', () => { clusterDetails: { name: 'cluster1', url: 'https://localhost:9999', - authMetadata: { - authProvider: 'serviceAccount', - serviceAccountToken: 'token', - }, + authMetadata: {}, }, + credential: 'token', objectTypesToFetch: new Set([ { group: '', @@ -765,12 +751,10 @@ describe('KubernetesFetcher', () => { clusterDetails: { name: 'cluster1', url: 'https://localhost:9999', - authMetadata: { - authProvider: 'serviceAccount', - serviceAccountToken: 'token', - }, + authMetadata: {}, caFile: '/path/to/ca.crt', }, + credential: 'token', objectTypesToFetch: new Set([ { group: '', @@ -805,12 +789,10 @@ describe('KubernetesFetcher', () => { clusterDetails: { name: 'cluster1', url: 'https://localhost:9999', - authMetadata: { - authProvider: 'serviceAccount', - serviceAccountToken: 'token', - }, + authMetadata: {}, skipTLSVerify: true, }, + credential: 'token', objectTypesToFetch: new Set([ { group: '', @@ -858,11 +840,9 @@ describe('KubernetesFetcher', () => { clusterDetails: { name: 'cluster1', url: 'http://localhost:9999', - authMetadata: { - authProvider: 'serviceAccount', - serviceAccountToken: 'token', - }, + authMetadata: {}, }, + credential: 'token', objectTypesToFetch: OBJECTS_TO_FETCH, labelSelector: '', namespace: 'some-namespace', @@ -898,14 +878,15 @@ describe('KubernetesFetcher', () => { }); }); describe('Backstage not running on k8s', () => { - it('fails if cluster details has no token', () => { + it('fails if no credential is provided', () => { const result = sut.fetchObjectsForService({ serviceId: 'some-service', clusterDetails: { name: 'unauthenticated-cluster', url: 'http://ignored', - authMetadata: { authProvider: 'serviceAccount' }, + authMetadata: {}, }, + credential: undefined, // no token objectTypesToFetch: OBJECTS_TO_FETCH, labelSelector: '', customResources: [], @@ -947,8 +928,9 @@ describe('KubernetesFetcher', () => { clusterDetails: { name: 'overridden-to-in-cluster', url: 'http://ignored', - authMetadata: { authProvider: 'serviceAccount' }, + authMetadata: {}, }, + credential: undefined, // no token objectTypesToFetch: new Set([ { group: '', @@ -1043,11 +1025,9 @@ describe('KubernetesFetcher', () => { { name: 'cluster1', url: 'http://localhost:9999', - authMetadata: { - authProvider: 'serviceAccount', - serviceAccountToken: 'token', - }, + authMetadata: {}, }, + 'token', new Set(['ns-a']), ); expect(result).toMatchObject({ @@ -1132,11 +1112,9 @@ describe('KubernetesFetcher', () => { { name: 'cluster1', url: 'http://localhost:9999', - authMetadata: { - authProvider: 'serviceAccount', - serviceAccountToken: 'token', - }, + authMetadata: {}, }, + 'token', new Set(['ns-a', 'ns-b']), ); diff --git a/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts b/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts index 167452d362..61a68a06a5 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts @@ -32,7 +32,9 @@ import { KubernetesFetcher, ObjectFetchParams, } from '../types/types'; +import { KubernetesCredential } from '../auth/types'; import { + ANNOTATION_KUBERNETES_AUTH_PROVIDER, FetchResponse, KubernetesFetchError, KubernetesErrorTypes, @@ -95,6 +97,7 @@ export class KubernetesClientBasedFetcher implements KubernetesFetcher { .map(({ objectType, group, apiVersion, plural }) => this.fetchResource( params.clusterDetails, + params.credential, group, apiVersion, plural, @@ -125,6 +128,7 @@ export class KubernetesClientBasedFetcher implements KubernetesFetcher { fetchPodMetricsByNamespaces( clusterDetails: ClusterDetails, + credential: KubernetesCredential, namespaces: Set, labelSelector?: string, ): Promise { @@ -132,13 +136,22 @@ export class KubernetesClientBasedFetcher implements KubernetesFetcher { const [podMetrics, podList] = await Promise.all([ this.fetchResource( clusterDetails, + credential, 'metrics.k8s.io', 'v1beta1', 'pods', ns, labelSelector, ), - this.fetchResource(clusterDetails, '', 'v1', 'pods', ns, labelSelector), + this.fetchResource( + clusterDetails, + credential, + '', + 'v1', + 'pods', + ns, + labelSelector, + ), ]); if (podMetrics.ok && podList.ok) { return topPods( @@ -183,6 +196,7 @@ export class KubernetesClientBasedFetcher implements KubernetesFetcher { private fetchResource( clusterDetails: ClusterDetails, + credential: KubernetesCredential, group: string, apiVersion: string, plural: string, @@ -201,10 +215,11 @@ export class KubernetesClientBasedFetcher implements KubernetesFetcher { let url: URL; let requestInit: RequestInit; if ( - clusterDetails.authMetadata.serviceAccountToken || - clusterDetails.authMetadata.authProvider === 'localKubectlProxy' + credential || + clusterDetails.authMetadata[ANNOTATION_KUBERNETES_AUTH_PROVIDER] === + 'localKubectlProxy' ) { - [url, requestInit] = this.fetchArgsFromClusterDetails(clusterDetails); + [url, requestInit] = this.fetchArgs(clusterDetails, credential); } else if (fs.pathExistsSync(Config.SERVICEACCOUNT_TOKEN_PATH)) { [url, requestInit] = this.fetchArgsInCluster(); } else { @@ -228,15 +243,16 @@ export class KubernetesClientBasedFetcher implements KubernetesFetcher { return fetch(url, requestInit); } - private fetchArgsFromClusterDetails( + private fetchArgs( clusterDetails: ClusterDetails, + credential: KubernetesCredential, ): [URL, RequestInit] { const requestInit: RequestInit = { method: 'GET', headers: { Accept: 'application/json', 'Content-Type': 'application/json', - Authorization: `Bearer ${clusterDetails.authMetadata.serviceAccountToken}`, + ...(credential && { Authorization: `Bearer ${credential}` }), }, }; diff --git a/plugins/kubernetes-backend/src/service/KubernetesProxy.test.ts b/plugins/kubernetes-backend/src/service/KubernetesProxy.test.ts index 7f28ceff4e..e1770d45b6 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesProxy.test.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesProxy.test.ts @@ -51,7 +51,7 @@ describe('KubernetesProxy', () => { const logger = getVoidLogger(); const clusterSupplier: jest.Mocked = { - getClusters: jest.fn(), + getClusters: jest.fn, []>(), }; const permissionApi: jest.Mocked = { @@ -60,7 +60,7 @@ describe('KubernetesProxy', () => { }; const authStrategy: jest.Mocked = { - decorateClusterDetailsWithAuth: jest.fn(), + getCredential: jest.fn(), validate: jest.fn(), }; @@ -146,17 +146,14 @@ describe('KubernetesProxy', () => { { name: 'local', url: 'http:/localhost:8001', - authMetadata: { authProvider: 'localKubectlProxy' }, + authMetadata: {}, skipMetricsLookup: true, - } as ClusterDetails, + }, { name: 'cluster1', url: 'https://localhost:9999', - authMetadata: { - authProvider: 'googleServiceAccount', - serviceAccountToken: 'tokenA', - }, - } as ClusterDetails, + authMetadata: {}, + }, ]); const req = buildMockRequest(undefined, 'api'); @@ -172,11 +169,8 @@ describe('KubernetesProxy', () => { { name: 'cluster1', url: 'https://localhost:9999', - authMetadata: { - authProvider: 'googleServiceAccount', - serviceAccountToken: 'tokenA', - }, - } as ClusterDetails, + authMetadata: {}, + }, ]); const req = buildMockRequest('test', 'api'); @@ -203,15 +197,9 @@ describe('KubernetesProxy', () => { { name: 'cluster1', url: 'https://localhost:9999', - authMetadata: { authProvider: 'serviceAccount' }, + authMetadata: {}, }, - ] as ClusterDetails[]); - - authStrategy.decorateClusterDetailsWithAuth.mockResolvedValue({ - name: 'cluster1', - url: 'https://localhost:9999', - authMetadata: { authProvider: 'serviceAccount' }, - } as ClusterDetails); + ]); worker.use( rest.get('https://localhost:9999/api', (_: any, res: any, ctx: any) => @@ -247,15 +235,9 @@ describe('KubernetesProxy', () => { { name: 'cluster1', url: 'https://localhost:9999', - authMetadata: { authProvider: 'serviceAccount' }, + authMetadata: {}, }, - ] as ClusterDetails[]); - - authStrategy.decorateClusterDetailsWithAuth.mockResolvedValue({ - name: 'cluster1', - url: 'https://localhost:9999', - authMetadata: { authProvider: 'serviceAccount' }, - } as ClusterDetails); + ]); worker.use( rest.get('https://localhost:9999/api', (_: any, res: any, ctx: any) => @@ -291,12 +273,9 @@ describe('KubernetesProxy', () => { { name: 'cluster1', url: 'http://localhost:9999', - authMetadata: { authProvider: '' }, + authMetadata: {}, }, ]); - authStrategy.decorateClusterDetailsWithAuth.mockImplementation( - async x => x, - ); const requestPromise = setupProxyPromise({ proxyPath: '/mountpath', @@ -310,7 +289,7 @@ describe('KubernetesProxy', () => { expect(response.status).toEqual(200); }); - it('should default to using an authStrategy-provided serviceAccountToken as authorization headers to kubeapi when backstage-kubernetes-auth field is not provided', async () => { + it('should default to using a strategy-provided bearer token as authorization headers to kubeapi when backstage-kubernetes-auth field is not provided', async () => { worker.use( rest.get( 'https://localhost:9999/api/v1/namespaces', @@ -342,18 +321,11 @@ describe('KubernetesProxy', () => { { name: 'cluster1', url: 'https://localhost:9999', - authMetadata: { authProvider: 'serviceAccount' }, + authMetadata: {}, }, - ] as ClusterDetails[]); + ]); - authStrategy.decorateClusterDetailsWithAuth.mockResolvedValue({ - name: 'cluster1', - url: 'https://localhost:9999', - authMetadata: { - authProvider: 'serviceAccount', - serviceAccountToken: 'strategy-provided-token', - }, - } as ClusterDetails); + authStrategy.getCredential.mockResolvedValue('strategy-provided-token'); const requestPromise = setupProxyPromise({ proxyPath: '/mountpath', @@ -393,18 +365,11 @@ describe('KubernetesProxy', () => { { name: 'cluster1', url: 'https://localhost:9999', - authMetadata: { authProvider: 'googleServiceAccount' }, + authMetadata: {}, }, - ] as ClusterDetails[]); + ]); - authStrategy.decorateClusterDetailsWithAuth.mockResolvedValue({ - name: 'cluster1', - url: 'https://localhost:9999', - authMetadata: { - authProvider: 'googleServiceAccount', - serviceAccountToken: 'my-token', - }, - } as ClusterDetails); + authStrategy.getCredential.mockResolvedValue('my-token'); const requestPromise = setupProxyPromise({ proxyPath: '/mountpath', @@ -449,18 +414,11 @@ describe('KubernetesProxy', () => { { name: 'cluster1', url: 'https://localhost:9999', - authMetadata: { authProvider: 'googleServiceAccount' }, + authMetadata: {}, }, - ] as ClusterDetails[]); + ]); - authStrategy.decorateClusterDetailsWithAuth.mockResolvedValue({ - name: 'cluster1', - url: 'https://localhost:9999', - authMetadata: { - authProvider: 'googleServiceAccount', - serviceAccountToken: 'tokenA', - }, - } as ClusterDetails); + authStrategy.getCredential.mockResolvedValue('tokenA'); const requestPromise = setupProxyPromise({ proxyPath: '/mountpath', @@ -508,9 +466,9 @@ describe('KubernetesProxy', () => { { name: 'cluster1', url: 'https://localhost:9999', - authMetadata: { authProvider: 'googleServiceAccount' }, + authMetadata: {}, }, - ] as ClusterDetails[]); + ]); const requestPromise = setupProxyPromise({ proxyPath: '/mountpath', @@ -524,9 +482,7 @@ describe('KubernetesProxy', () => { const response = await requestPromise; - expect(authStrategy.decorateClusterDetailsWithAuth).toHaveBeenCalledTimes( - 0, - ); + expect(authStrategy.getCredential).toHaveBeenCalledTimes(0); expect(response.status).toEqual(200); expect(response.body).toStrictEqual({ kind: 'NamespaceList', @@ -602,16 +558,11 @@ describe('KubernetesProxy', () => { { name: 'cluster1', url: 'https://localhost:9999', - authMetadata: { - authProvider: 'google', - serviceAccountToken: 'client-side-token', - }, + authMetadata: {}, }, - ] as ClusterDetails[]); + ]); - authStrategy.decorateClusterDetailsWithAuth.mockRejectedValue( - Error('some internal error'), - ); + authStrategy.getCredential.mockRejectedValue(Error('some internal error')); const requestPromise = setupProxyPromise({ proxyPath: '/mountpath', @@ -648,14 +599,10 @@ describe('KubernetesProxy', () => { { name: 'cluster1', url: 'http://localhost:9999/subpath', - authMetadata: { authProvider: '' }, + authMetadata: {}, }, ]); - authStrategy.decorateClusterDetailsWithAuth.mockImplementation( - async x => x, - ); - const requestPromise = setupProxyPromise({ proxyPath: '/mountpath', requestPath: '/api/v1/namespaces', @@ -701,19 +648,11 @@ describe('KubernetesProxy', () => { { name: 'cluster1', url: 'https://localhost:9999', - serviceAccountToken: '', - authProvider: 'serviceAccount', + authMetadata: {}, caFile: resolvePath(__dirname, '__fixtures__/mock-ca.crt'), }, ] as ClusterDetails[]); - authTranslator.decorateClusterDetailsWithAuth.mockResolvedValue({ - name: 'cluster1', - url: 'https://localhost:9999', - serviceAccountToken: '', - authProvider: 'serviceAccount', - } as ClusterDetails); - worker.use( rest.get('https://localhost:9999/api', (_: any, res: any, ctx: any) => res(ctx.status(299), ctx.json(apiResponse)), @@ -793,15 +732,9 @@ describe('KubernetesProxy', () => { { name: 'local', url: `http://localhost:${wsPort}`, - authMetadata: { authProvider: 'serviceAccount' }, + authMetadata: {}, }, - ] as ClusterDetails[]); - - authStrategy.decorateClusterDetailsWithAuth.mockResolvedValue({ - name: 'local', - url: `http://localhost:${wsPort}`, - authMetadata: { authProvider: 'serviceAccount' }, - } as ClusterDetails); + ]); const wsProxyAddress = `ws://127.0.0.1:${proxyPort}${proxyPath}${wsPath}`; const wsAddress = `ws://localhost:${wsPort}${wsPath}`; diff --git a/plugins/kubernetes-backend/src/service/KubernetesProxy.ts b/plugins/kubernetes-backend/src/service/KubernetesProxy.ts index d895a38e6e..ce96659c30 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesProxy.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesProxy.ts @@ -112,9 +112,9 @@ export class KubernetesProxy { if (authHeader) { req.headers.authorization = authHeader; } else { - const serviceAccountToken = await this.getClusterForRequest(req) - .then(cd => this.authStrategy.decorateClusterDetailsWithAuth(cd, {})) - .then(cd => cd.authMetadata.serviceAccountToken); + const serviceAccountToken = await this.getClusterForRequest(req).then( + cd => this.authStrategy.getCredential(cd, {}), + ); if (serviceAccountToken) { req.headers.authorization = `Bearer ${serviceAccountToken}`; } diff --git a/plugins/kubernetes-backend/src/types/types.ts b/plugins/kubernetes-backend/src/types/types.ts index 48bed695d0..3767791589 100644 --- a/plugins/kubernetes-backend/src/types/types.ts +++ b/plugins/kubernetes-backend/src/types/types.ts @@ -26,6 +26,7 @@ import type { ObjectsByEntityResponse, } from '@backstage/plugin-kubernetes-common'; import { Config } from '@backstage/config'; +import { KubernetesCredential } from '../auth/types'; /** * @@ -34,6 +35,7 @@ import { Config } from '@backstage/config'; export interface ObjectFetchParams { serviceId: string; clusterDetails: ClusterDetails; + credential: KubernetesCredential; objectTypesToFetch: Set; labelSelector: string; customResources: CustomResource[]; @@ -51,6 +53,7 @@ export interface KubernetesFetcher { ): Promise; fetchPodMetricsByNamespaces( clusterDetails: ClusterDetails, + credential: KubernetesCredential, namespaces: Set, labelSelector?: string, ): Promise; From 31dc20dbe715cf3bdee70213ede80549a5f39850 Mon Sep 17 00:00:00 2001 From: Jamie Klassen Date: Fri, 8 Sep 2023 11:53:41 -0400 Subject: [PATCH 15/20] replace noop with distinct strategies one for serviceaccount and one for anonymous (used by localKubectlProxy) Signed-off-by: Jamie Klassen --- plugins/kubernetes-backend/api-report.md | 24 ++++++++++----- .../src/auth/AnonymousStrategy.ts | 29 +++++++++++++++++++ ...pStrategy.ts => ServiceAccountStrategy.ts} | 2 +- plugins/kubernetes-backend/src/auth/index.ts | 3 +- .../src/service/KubernetesBuilder.ts | 7 +++-- .../src/service/KubernetesProxy.test.ts | 4 +-- 6 files changed, 54 insertions(+), 15 deletions(-) create mode 100644 plugins/kubernetes-backend/src/auth/AnonymousStrategy.ts rename plugins/kubernetes-backend/src/auth/{NoopStrategy.ts => ServiceAccountStrategy.ts} (92%) diff --git a/plugins/kubernetes-backend/api-report.md b/plugins/kubernetes-backend/api-report.md index 1c5cb766dc..dad792843f 100644 --- a/plugins/kubernetes-backend/api-report.md +++ b/plugins/kubernetes-backend/api-report.md @@ -32,6 +32,14 @@ export class AksStrategy implements AuthenticationStrategy { validate(_: AuthMetadata): void; } +// @public (undocumented) +export class AnonymousStrategy implements AuthenticationStrategy { + // (undocumented) + getCredential(): Promise; + // (undocumented) + validate(): void; +} + // @public (undocumented) export interface AuthenticationStrategy { // (undocumented) @@ -383,14 +391,6 @@ export interface KubernetesServiceLocator { }>; } -// @public (undocumented) -export class NoopStrategy implements AuthenticationStrategy { - // (undocumented) - getCredential(): Promise; - // (undocumented) - validate(): void; -} - // @public (undocumented) export interface ObjectFetchParams { // (undocumented) @@ -451,6 +451,14 @@ export interface RouterOptions { permissions: PermissionEvaluator; } +// @public (undocumented) +export class ServiceAccountStrategy implements AuthenticationStrategy { + // (undocumented) + getCredential(clusterDetails: ClusterDetails): Promise; + // (undocumented) + validate(): void; +} + // @public (undocumented) export type ServiceLocatorMethod = 'multiTenant' | 'http'; diff --git a/plugins/kubernetes-backend/src/auth/AnonymousStrategy.ts b/plugins/kubernetes-backend/src/auth/AnonymousStrategy.ts new file mode 100644 index 0000000000..2b3215f118 --- /dev/null +++ b/plugins/kubernetes-backend/src/auth/AnonymousStrategy.ts @@ -0,0 +1,29 @@ +/* + * Copyright 2020 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 { AuthenticationStrategy, KubernetesCredential } from './types'; + +/** + * + * @public + */ +export class AnonymousStrategy implements AuthenticationStrategy { + public async getCredential(): Promise { + return undefined; + } + + public validate() {} +} diff --git a/plugins/kubernetes-backend/src/auth/NoopStrategy.ts b/plugins/kubernetes-backend/src/auth/ServiceAccountStrategy.ts similarity index 92% rename from plugins/kubernetes-backend/src/auth/NoopStrategy.ts rename to plugins/kubernetes-backend/src/auth/ServiceAccountStrategy.ts index 1af8f09cf8..29462dc5c7 100644 --- a/plugins/kubernetes-backend/src/auth/NoopStrategy.ts +++ b/plugins/kubernetes-backend/src/auth/ServiceAccountStrategy.ts @@ -21,7 +21,7 @@ import { ClusterDetails } from '../types/types'; * * @public */ -export class NoopStrategy implements AuthenticationStrategy { +export class ServiceAccountStrategy implements AuthenticationStrategy { public async getCredential( clusterDetails: ClusterDetails, ): Promise { diff --git a/plugins/kubernetes-backend/src/auth/index.ts b/plugins/kubernetes-backend/src/auth/index.ts index 2caefc3a12..bcdd620243 100644 --- a/plugins/kubernetes-backend/src/auth/index.ts +++ b/plugins/kubernetes-backend/src/auth/index.ts @@ -15,11 +15,12 @@ */ export * from './AksStrategy'; +export * from './AnonymousStrategy'; export * from './AwsIamStrategy'; export * from './AzureIdentityStrategy'; export * from './GoogleStrategy'; export * from './GoogleServiceAccountStrategy'; export * from './DispatchStrategy'; -export * from './NoopStrategy'; +export * from './ServiceAccountStrategy'; export * from './OidcStrategy'; export * from './types'; diff --git a/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts b/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts index 62cc53ed4a..f3e71e8dbc 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts @@ -30,9 +30,10 @@ import { Logger } from 'winston'; import { getCombinedClusterSupplier } from '../cluster-locator'; import { AuthenticationStrategy, + AnonymousStrategy, DispatchStrategy, GoogleStrategy, - NoopStrategy, + ServiceAccountStrategy, AwsIamStrategy, GoogleServiceAccountStrategy, AzureIdentityStrategy, @@ -369,9 +370,9 @@ export class KubernetesBuilder { azure: new AzureIdentityStrategy(this.env.logger), google: new GoogleStrategy(), googleServiceAccount: new GoogleServiceAccountStrategy(), - localKubectlProxy: new NoopStrategy(), + localKubectlProxy: new AnonymousStrategy(), oidc: new OidcStrategy(), - serviceAccount: new NoopStrategy(), + serviceAccount: new ServiceAccountStrategy(), }; return this.authStrategyMap; } diff --git a/plugins/kubernetes-backend/src/service/KubernetesProxy.test.ts b/plugins/kubernetes-backend/src/service/KubernetesProxy.test.ts index e1770d45b6..9c3e7e9187 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesProxy.test.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesProxy.test.ts @@ -33,7 +33,7 @@ import request from 'supertest'; import { AddressInfo, WebSocket, WebSocketServer } from 'ws'; import { LocalKubectlProxyClusterLocator } from '../cluster-locator/LocalKubectlProxyLocator'; -import { AuthenticationStrategy, NoopStrategy } from '../auth'; +import { AuthenticationStrategy, AnonymousStrategy } from '../auth'; import { ClusterDetails, KubernetesClustersSupplier } from '../types/types'; import { APPLICATION_JSON, @@ -495,7 +495,7 @@ describe('KubernetesProxy', () => { proxy = new KubernetesProxy({ logger: getVoidLogger(), clusterSupplier: new LocalKubectlProxyClusterLocator(), - authStrategy: new NoopStrategy(), + authStrategy: new AnonymousStrategy(), }); worker.use( From 1df3afb21e8dd2a0481e6b5bb54229e9792efc94 Mon Sep 17 00:00:00 2001 From: Jamie Klassen Date: Fri, 8 Sep 2023 13:11:07 -0400 Subject: [PATCH 16/20] make KubernetesCredential a tagged union This is easier to read than using primitives like `undefined` and `string` to represent these states. Signed-off-by: Jamie Klassen --- plugins/kubernetes-backend/api-report.md | 9 +++- .../src/auth/AksStrategy.test.ts | 5 ++- .../src/auth/AksStrategy.ts | 3 +- .../src/auth/AnonymousStrategy.ts | 2 +- .../src/auth/AwsIamStrategy.test.ts | 21 ++++++---- .../src/auth/AwsIamStrategy.ts | 15 ++++--- .../src/auth/AzureIdentityStrategy.test.ts | 22 +++++----- .../src/auth/AzureIdentityStrategy.ts | 6 ++- .../src/auth/DispatchStrategy.test.ts | 10 ++++- .../src/auth/GoogleServiceAccountStrategy.ts | 6 +-- .../src/auth/GoogleStrategy.ts | 6 +-- .../src/auth/OidcStrategy.test.ts | 5 ++- .../src/auth/OidcStrategy.ts | 6 +-- .../src/auth/ServiceAccountStrategy.ts | 3 +- plugins/kubernetes-backend/src/auth/types.ts | 4 +- .../service/KubernetesFanOutHandler.test.ts | 23 ++++++++--- .../src/service/KubernetesFetcher.test.ts | 34 +++++++-------- .../src/service/KubernetesFetcher.ts | 6 ++- .../src/service/KubernetesProxy.test.ts | 41 ++++++++++++++----- .../src/service/KubernetesProxy.ts | 10 ++--- 20 files changed, 151 insertions(+), 86 deletions(-) diff --git a/plugins/kubernetes-backend/api-report.md b/plugins/kubernetes-backend/api-report.md index dad792843f..5aa124394a 100644 --- a/plugins/kubernetes-backend/api-report.md +++ b/plugins/kubernetes-backend/api-report.md @@ -278,7 +278,14 @@ export interface KubernetesClustersSupplier { } // @public -export type KubernetesCredential = string | undefined; +export type KubernetesCredential = + | { + type: 'bearer token'; + token: string; + } + | { + type: 'anonymous'; + }; // @public (undocumented) export interface KubernetesEnvironment { diff --git a/plugins/kubernetes-backend/src/auth/AksStrategy.test.ts b/plugins/kubernetes-backend/src/auth/AksStrategy.test.ts index 373d053059..dfd8c85ce4 100644 --- a/plugins/kubernetes-backend/src/auth/AksStrategy.test.ts +++ b/plugins/kubernetes-backend/src/auth/AksStrategy.test.ts @@ -24,6 +24,9 @@ describe('AksStrategy', () => { { aks: 'aksToken' }, ); - expect(credential).toBe('aksToken'); + expect(credential).toStrictEqual({ + type: 'bearer token', + token: 'aksToken', + }); }); }); diff --git a/plugins/kubernetes-backend/src/auth/AksStrategy.ts b/plugins/kubernetes-backend/src/auth/AksStrategy.ts index 01fe8ffc8e..db4d423060 100644 --- a/plugins/kubernetes-backend/src/auth/AksStrategy.ts +++ b/plugins/kubernetes-backend/src/auth/AksStrategy.ts @@ -26,7 +26,8 @@ export class AksStrategy implements AuthenticationStrategy { _: ClusterDetails, requestAuth: KubernetesRequestAuth, ): Promise { - return requestAuth.aks; + const token = requestAuth.aks; + return token ? { type: 'bearer token', token } : { type: 'anonymous' }; } public validate(_: AuthMetadata) {} } diff --git a/plugins/kubernetes-backend/src/auth/AnonymousStrategy.ts b/plugins/kubernetes-backend/src/auth/AnonymousStrategy.ts index 2b3215f118..20e688c2a3 100644 --- a/plugins/kubernetes-backend/src/auth/AnonymousStrategy.ts +++ b/plugins/kubernetes-backend/src/auth/AnonymousStrategy.ts @@ -22,7 +22,7 @@ import { AuthenticationStrategy, KubernetesCredential } from './types'; */ export class AnonymousStrategy implements AuthenticationStrategy { public async getCredential(): Promise { - return undefined; + return { type: 'anonymous' }; } public validate() {} diff --git a/plugins/kubernetes-backend/src/auth/AwsIamStrategy.test.ts b/plugins/kubernetes-backend/src/auth/AwsIamStrategy.test.ts index e142717541..873c78e177 100644 --- a/plugins/kubernetes-backend/src/auth/AwsIamStrategy.test.ts +++ b/plugins/kubernetes-backend/src/auth/AwsIamStrategy.test.ts @@ -65,9 +65,10 @@ describe('AwsIamStrategy tests', () => { url: '', authMetadata: {}, }); - expect(credential).toEqual( - 'k8s-aws-v1.aHR0cHM6Ly9odHRwczovL2V4YW1wbGUuY29tL2FzZGY_', - ); + expect(credential).toEqual({ + type: 'bearer token', + token: 'k8s-aws-v1.aHR0cHM6Ly9odHRwczovL2V4YW1wbGUuY29tL2FzZGY_', + }); }); it('returns a signed url for AWS credentials with assume role', async () => { @@ -81,9 +82,10 @@ describe('AwsIamStrategy tests', () => { }, }); - expect(credential).toEqual( - 'k8s-aws-v1.aHR0cHM6Ly9odHRwczovL2V4YW1wbGUuY29tL2FzZGY_', - ); + expect(credential).toEqual({ + type: 'bearer token', + token: 'k8s-aws-v1.aHR0cHM6Ly9odHRwczovL2V4YW1wbGUuY29tL2FzZGY_', + }); expect(fromTemporaryCredentials).toHaveBeenCalledWith({ clientConfig: { region: 'us-east-1', @@ -109,9 +111,10 @@ describe('AwsIamStrategy tests', () => { [ANNOTATION_KUBERNETES_AWS_EXTERNAL_ID]: 'external-id', }, }); - expect(credential).toEqual( - 'k8s-aws-v1.aHR0cHM6Ly9odHRwczovL2V4YW1wbGUuY29tL2FzZGY_', - ); + expect(credential).toEqual({ + type: 'bearer token', + token: 'k8s-aws-v1.aHR0cHM6Ly9odHRwczovL2V4YW1wbGUuY29tL2FzZGY_', + }); expect(fromTemporaryCredentials).toHaveBeenCalledWith({ clientConfig: { region: 'us-east-1', diff --git a/plugins/kubernetes-backend/src/auth/AwsIamStrategy.ts b/plugins/kubernetes-backend/src/auth/AwsIamStrategy.ts index 367df5271a..90ea3ba1b2 100644 --- a/plugins/kubernetes-backend/src/auth/AwsIamStrategy.ts +++ b/plugins/kubernetes-backend/src/auth/AwsIamStrategy.ts @@ -51,14 +51,17 @@ export class AwsIamStrategy implements AuthenticationStrategy { this.credsManager = DefaultAwsCredentialsManager.fromConfig(opts.config); } - public getCredential( + public async getCredential( clusterDetails: ClusterDetails, ): Promise { - return this.getBearerToken( - clusterDetails.name, - clusterDetails.authMetadata[ANNOTATION_KUBERNETES_AWS_ASSUME_ROLE], - clusterDetails.authMetadata[ANNOTATION_KUBERNETES_AWS_EXTERNAL_ID], - ); + return { + type: 'bearer token', + token: await this.getBearerToken( + clusterDetails.name, + clusterDetails.authMetadata[ANNOTATION_KUBERNETES_AWS_ASSUME_ROLE], + clusterDetails.authMetadata[ANNOTATION_KUBERNETES_AWS_EXTERNAL_ID], + ), + }; } public validate() {} diff --git a/plugins/kubernetes-backend/src/auth/AzureIdentityStrategy.test.ts b/plugins/kubernetes-backend/src/auth/AzureIdentityStrategy.test.ts index 398bb99516..7e1f99aeeb 100644 --- a/plugins/kubernetes-backend/src/auth/AzureIdentityStrategy.test.ts +++ b/plugins/kubernetes-backend/src/auth/AzureIdentityStrategy.test.ts @@ -51,7 +51,7 @@ describe('AzureIdentityStrategy tests', () => { ); const credential = await strategy.getCredential(); - expect(credential).toEqual('MY_TOKEN_1'); + expect(credential).toEqual({ type: 'bearer token', token: 'MY_TOKEN_1' }); }); it('should re-use token before expiry', async () => { @@ -61,10 +61,10 @@ describe('AzureIdentityStrategy tests', () => { ); const credential = await strategy.getCredential(); - expect(credential).toEqual('MY_TOKEN_1'); + expect(credential).toEqual({ type: 'bearer token', token: 'MY_TOKEN_1' }); const credential2 = await strategy.getCredential(); - expect(credential2).toEqual('MY_TOKEN_1'); + expect(credential2).toEqual({ type: 'bearer token', token: 'MY_TOKEN_1' }); }); it('should issue new token 15 minutes befory expiry', async () => { @@ -76,12 +76,12 @@ describe('AzureIdentityStrategy tests', () => { ); const credential = await strategy.getCredential(); - expect(credential).toEqual('MY_TOKEN_1'); + expect(credential).toEqual({ type: 'bearer token', token: 'MY_TOKEN_1' }); jest.setSystemTime(Date.now() + 2 * 60 * 1000); // advance time by 2mins const credential2 = await strategy.getCredential(); - expect(credential2).toEqual('MY_TOKEN_2'); + expect(credential2).toEqual({ type: 'bearer token', token: 'MY_TOKEN_2' }); }); it('should re-use existing token if there is afailure', async () => { @@ -93,20 +93,20 @@ describe('AzureIdentityStrategy tests', () => { ); const credential = await strategy.getCredential(); - expect(credential).toEqual('MY_TOKEN_1'); + expect(credential).toEqual({ type: 'bearer token', token: 'MY_TOKEN_1' }); jest.setSystemTime(Date.now() + 2 * 60 * 1000); // advance time by 2min const credential2 = await strategy.getCredential(); - expect(credential2).toEqual('MY_TOKEN_2'); + expect(credential2).toEqual({ type: 'bearer token', token: 'MY_TOKEN_2' }); jest.setSystemTime(Date.now() + 2 * 60 * 1000); // advance time by 2min const credential3 = await strategy.getCredential(); - expect(credential3).toEqual('MY_TOKEN_2'); + expect(credential3).toEqual({ type: 'bearer token', token: 'MY_TOKEN_2' }); const credential4 = await strategy.getCredential(); - expect(credential4).toEqual('MY_TOKEN_4'); + expect(credential4).toEqual({ type: 'bearer token', token: 'MY_TOKEN_4' }); }); it('should throw if existing token expired and failed to fetch a new one', async () => { @@ -118,12 +118,12 @@ describe('AzureIdentityStrategy tests', () => { ); const credential = await strategy.getCredential(); - expect(credential).toEqual('MY_TOKEN_1'); + expect(credential).toEqual({ type: 'bearer token', token: 'MY_TOKEN_1' }); jest.setSystemTime(Date.now() + 2 * 60 * 1000); // advance time by 2min const credential2 = await strategy.getCredential(); - expect(credential2).toEqual('MY_TOKEN_2'); + expect(credential2).toEqual({ type: 'bearer token', token: 'MY_TOKEN_2' }); jest.setSystemTime(Date.now() + 17 * 60 * 1000); // advance time by 17min diff --git a/plugins/kubernetes-backend/src/auth/AzureIdentityStrategy.ts b/plugins/kubernetes-backend/src/auth/AzureIdentityStrategy.ts index f4dbc45612..cca0f3dede 100644 --- a/plugins/kubernetes-backend/src/auth/AzureIdentityStrategy.ts +++ b/plugins/kubernetes-backend/src/auth/AzureIdentityStrategy.ts @@ -39,14 +39,16 @@ export class AzureIdentityStrategy implements AuthenticationStrategy { public async getCredential(): Promise { if (!this.tokenRequiresRefresh()) { - return this.accessToken.token; + return { type: 'bearer token', token: this.accessToken.token }; } if (!this.newTokenPromise) { this.newTokenPromise = this.fetchNewToken(); } - return this.newTokenPromise; + return this.newTokenPromise + ? { type: 'bearer token', token: await this.newTokenPromise } + : { type: 'anonymous' }; } public validate() {} diff --git a/plugins/kubernetes-backend/src/auth/DispatchStrategy.test.ts b/plugins/kubernetes-backend/src/auth/DispatchStrategy.test.ts index 0675b21a9a..f7db6ccd95 100644 --- a/plugins/kubernetes-backend/src/auth/DispatchStrategy.test.ts +++ b/plugins/kubernetes-backend/src/auth/DispatchStrategy.test.ts @@ -43,7 +43,10 @@ describe('getCredential', () => { name: 'randomName', authMetadata: { [ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'google' }, }; - mockStrategy.getCredential.mockResolvedValue('added by mock strategy'); + mockStrategy.getCredential.mockResolvedValue({ + type: 'bearer token', + token: 'added by mock strategy', + }); const returnedValue = await strategy.getCredential( clusterDetails, @@ -54,7 +57,10 @@ describe('getCredential', () => { clusterDetails, authObject, ); - expect(returnedValue).toBe('added by mock strategy'); + expect(returnedValue).toStrictEqual({ + type: 'bearer token', + token: 'added by mock strategy', + }); }); it('throws an error when asked for a strategy for an unsupported auth type', () => { diff --git a/plugins/kubernetes-backend/src/auth/GoogleServiceAccountStrategy.ts b/plugins/kubernetes-backend/src/auth/GoogleServiceAccountStrategy.ts index 66e0286372..4aeba504af 100644 --- a/plugins/kubernetes-backend/src/auth/GoogleServiceAccountStrategy.ts +++ b/plugins/kubernetes-backend/src/auth/GoogleServiceAccountStrategy.ts @@ -23,14 +23,14 @@ import * as container from '@google-cloud/container'; export class GoogleServiceAccountStrategy implements AuthenticationStrategy { public async getCredential(): Promise { const client = new container.v1.ClusterManagerClient(); - const accessToken = await client.auth.getAccessToken(); + const token = await client.auth.getAccessToken(); - if (!accessToken) { + if (!token) { throw new Error( 'Unable to obtain access token for the current Google Application Default Credentials', ); } - return accessToken; + return { type: 'bearer token', token }; } public validate() {} diff --git a/plugins/kubernetes-backend/src/auth/GoogleStrategy.ts b/plugins/kubernetes-backend/src/auth/GoogleStrategy.ts index 3111e02651..ff8d02cb14 100644 --- a/plugins/kubernetes-backend/src/auth/GoogleStrategy.ts +++ b/plugins/kubernetes-backend/src/auth/GoogleStrategy.ts @@ -27,13 +27,13 @@ export class GoogleStrategy implements AuthenticationStrategy { _: ClusterDetails, requestAuth: KubernetesRequestAuth, ): Promise { - const authToken = requestAuth.google; - if (!authToken) { + const token = requestAuth.google; + if (!token) { throw new Error( 'Google token not found under auth.google in request body', ); } - return authToken; + return { type: 'bearer token', token }; } public validate(_: AuthMetadata) {} } diff --git a/plugins/kubernetes-backend/src/auth/OidcStrategy.test.ts b/plugins/kubernetes-backend/src/auth/OidcStrategy.test.ts index 0502f91fad..fe25b03c72 100644 --- a/plugins/kubernetes-backend/src/auth/OidcStrategy.test.ts +++ b/plugins/kubernetes-backend/src/auth/OidcStrategy.test.ts @@ -38,7 +38,10 @@ describe('OidcStrategy', () => { }, ); - expect(credential).toBe('fakeToken'); + expect(credential).toStrictEqual({ + type: 'bearer token', + token: 'fakeToken', + }); }); it('fails when token provider is not configured', async () => { diff --git a/plugins/kubernetes-backend/src/auth/OidcStrategy.ts b/plugins/kubernetes-backend/src/auth/OidcStrategy.ts index 21410686de..f5ab1cbd8d 100644 --- a/plugins/kubernetes-backend/src/auth/OidcStrategy.ts +++ b/plugins/kubernetes-backend/src/auth/OidcStrategy.ts @@ -38,14 +38,14 @@ export class OidcStrategy implements AuthenticationStrategy { ); } - const authToken = authConfig.oidc?.[oidcTokenProvider]; + const token = authConfig.oidc?.[oidcTokenProvider]; - if (!authToken) { + if (!token) { throw new Error( `Auth token not found under oidc.${oidcTokenProvider} in request body`, ); } - return authToken; + return { type: 'bearer token', token }; } public validate(authMetadata: AuthMetadata) { diff --git a/plugins/kubernetes-backend/src/auth/ServiceAccountStrategy.ts b/plugins/kubernetes-backend/src/auth/ServiceAccountStrategy.ts index 29462dc5c7..24ade91e48 100644 --- a/plugins/kubernetes-backend/src/auth/ServiceAccountStrategy.ts +++ b/plugins/kubernetes-backend/src/auth/ServiceAccountStrategy.ts @@ -25,7 +25,8 @@ export class ServiceAccountStrategy implements AuthenticationStrategy { public async getCredential( clusterDetails: ClusterDetails, ): Promise { - return clusterDetails.authMetadata.serviceAccountToken; + const token = clusterDetails.authMetadata.serviceAccountToken; + return token ? { type: 'bearer token', token } : { type: 'anonymous' }; } public validate() {} diff --git a/plugins/kubernetes-backend/src/auth/types.ts b/plugins/kubernetes-backend/src/auth/types.ts index e9a6ce3a16..4094f37c3b 100644 --- a/plugins/kubernetes-backend/src/auth/types.ts +++ b/plugins/kubernetes-backend/src/auth/types.ts @@ -21,7 +21,9 @@ import { KubernetesRequestAuth } from '@backstage/plugin-kubernetes-common'; * Authentication data used to make a request to Kubernetes * @public */ -export type KubernetesCredential = string | undefined; +export type KubernetesCredential = + | { type: 'bearer token'; token: string } + | { type: 'anonymous' }; /** * diff --git a/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.test.ts b/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.test.ts index 35c8ead904..75283a34f7 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.test.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.test.ts @@ -28,7 +28,10 @@ import { KubernetesClientBasedFetcher } from './KubernetesFetcher'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; -import { ObjectsByEntityResponse } from '@backstage/plugin-kubernetes-common'; +import { + KubernetesRequestAuth, + ObjectsByEntityResponse, +} from '@backstage/plugin-kubernetes-common'; import { Config, ConfigReader } from '@backstage/config'; import { Entity } from '@backstage/catalog-model'; @@ -186,7 +189,12 @@ describe('KubernetesFanOutHandler', () => { }, customResources: customResources, authStrategy: { - getCredential: jest.fn().mockResolvedValue(undefined), + getCredential: jest + .fn< + Promise, + [ClusterDetails, KubernetesRequestAuth] + >() + .mockResolvedValue({ type: 'anonymous' }), validate: jest.fn(), }, config, @@ -373,7 +381,7 @@ describe('KubernetesFanOutHandler', () => { expect(fetchPodMetricsByNamespaces).toHaveBeenCalledTimes(1); expect(fetchPodMetricsByNamespaces).toHaveBeenCalledWith( expect.anything(), - undefined, + { type: 'anonymous' }, new Set(['ns-test-component-test-cluster']), expect.anything(), ); @@ -665,7 +673,7 @@ describe('KubernetesFanOutHandler', () => { expect(fetchPodMetricsByNamespaces).toHaveBeenCalledTimes(1); expect(fetchPodMetricsByNamespaces).toHaveBeenCalledWith( expect.anything(), - undefined, + { type: 'anonymous' }, new Set(['ns-a', 'ns-b']), expect.anything(), ); @@ -1132,7 +1140,12 @@ describe('KubernetesFanOutHandler', () => { }, ], authStrategy: { - getCredential: jest.fn().mockResolvedValue('token'), + getCredential: jest + .fn< + Promise, + [ClusterDetails, KubernetesRequestAuth] + >() + .mockResolvedValue({ type: 'bearer token', token: 'token' }), validate: jest.fn(), }, config, diff --git a/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts b/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts index 6feac880ec..9f751ff8e1 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts @@ -141,7 +141,7 @@ describe('KubernetesFetcher', () => { url: 'http://localhost:9999', authMetadata: {}, }, - credential: 'token', + credential: { type: 'bearer token', token: 'token' }, objectTypesToFetch: OBJECTS_TO_FETCH, labelSelector: '', customResources: [], @@ -202,7 +202,7 @@ describe('KubernetesFetcher', () => { url: 'http://localhost:9999/k8s/clusters/1234', authMetadata: {}, }, - credential: 'token', + credential: { type: 'bearer token', token: 'token' }, objectTypesToFetch: OBJECTS_TO_FETCH, labelSelector: '', customResources: [], @@ -258,7 +258,7 @@ describe('KubernetesFetcher', () => { [ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'localKubectlProxy', }, }, - credential: undefined, + credential: { type: 'anonymous' }, objectTypesToFetch: new Set([ { group: '', @@ -315,7 +315,7 @@ describe('KubernetesFetcher', () => { url: 'http://localhost:9999', authMetadata: {}, }, - credential: 'token', + credential: { type: 'bearer token', token: 'token' }, objectTypesToFetch: OBJECTS_TO_FETCH, labelSelector: '', customResources: [], @@ -389,7 +389,7 @@ describe('KubernetesFetcher', () => { url: 'http://localhost:9999', authMetadata: {}, }, - credential: 'token', + credential: { type: 'bearer token', token: 'token' }, objectTypesToFetch: OBJECTS_TO_FETCH, labelSelector: '', customResources: [ @@ -481,7 +481,7 @@ describe('KubernetesFetcher', () => { url: 'http://localhost:9999', authMetadata: {}, }, - credential: 'token', + credential: { type: 'bearer token', token: 'token' }, objectTypesToFetch: OBJECTS_TO_FETCH, labelSelector: '', customResources: [], @@ -569,7 +569,7 @@ describe('KubernetesFetcher', () => { url: 'http://badurl.does.not.exist', authMetadata: {}, }, - credential: 'token', + credential: { type: 'bearer token', token: 'token' }, objectTypesToFetch: OBJECTS_TO_FETCH, labelSelector: '', customResources: [], @@ -606,7 +606,7 @@ describe('KubernetesFetcher', () => { url: 'http://localhost:9999', authMetadata: {}, }, - credential: 'token', + credential: { type: 'bearer token', token: 'token' }, objectTypesToFetch: OBJECTS_TO_FETCH, labelSelector: 'service-label=value', customResources: [], @@ -674,7 +674,7 @@ describe('KubernetesFetcher', () => { authMetadata: {}, caData: 'MOCKCA', }, - credential: 'token', + credential: { type: 'bearer token', token: 'token' }, objectTypesToFetch: new Set([ { group: '', @@ -710,7 +710,7 @@ describe('KubernetesFetcher', () => { url: 'https://localhost:9999', authMetadata: {}, }, - credential: 'token', + credential: { type: 'bearer token', token: 'token' }, objectTypesToFetch: new Set([ { group: '', @@ -754,7 +754,7 @@ describe('KubernetesFetcher', () => { authMetadata: {}, caFile: '/path/to/ca.crt', }, - credential: 'token', + credential: { type: 'bearer token', token: 'token' }, objectTypesToFetch: new Set([ { group: '', @@ -792,7 +792,7 @@ describe('KubernetesFetcher', () => { authMetadata: {}, skipTLSVerify: true, }, - credential: 'token', + credential: { type: 'bearer token', token: 'token' }, objectTypesToFetch: new Set([ { group: '', @@ -842,7 +842,7 @@ describe('KubernetesFetcher', () => { url: 'http://localhost:9999', authMetadata: {}, }, - credential: 'token', + credential: { type: 'bearer token', token: 'token' }, objectTypesToFetch: OBJECTS_TO_FETCH, labelSelector: '', namespace: 'some-namespace', @@ -886,7 +886,7 @@ describe('KubernetesFetcher', () => { url: 'http://ignored', authMetadata: {}, }, - credential: undefined, // no token + credential: { type: 'anonymous' }, objectTypesToFetch: OBJECTS_TO_FETCH, labelSelector: '', customResources: [], @@ -930,7 +930,7 @@ describe('KubernetesFetcher', () => { url: 'http://ignored', authMetadata: {}, }, - credential: undefined, // no token + credential: { type: 'anonymous' }, objectTypesToFetch: new Set([ { group: '', @@ -1027,7 +1027,7 @@ describe('KubernetesFetcher', () => { url: 'http://localhost:9999', authMetadata: {}, }, - 'token', + { type: 'bearer token', token: 'token' }, new Set(['ns-a']), ); expect(result).toMatchObject({ @@ -1114,7 +1114,7 @@ describe('KubernetesFetcher', () => { url: 'http://localhost:9999', authMetadata: {}, }, - 'token', + { type: 'bearer token', token: 'token' }, new Set(['ns-a', 'ns-b']), ); diff --git a/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts b/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts index 61a68a06a5..42556c8473 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts @@ -215,7 +215,7 @@ export class KubernetesClientBasedFetcher implements KubernetesFetcher { let url: URL; let requestInit: RequestInit; if ( - credential || + credential.type === 'bearer token' || clusterDetails.authMetadata[ANNOTATION_KUBERNETES_AUTH_PROVIDER] === 'localKubectlProxy' ) { @@ -252,7 +252,9 @@ export class KubernetesClientBasedFetcher implements KubernetesFetcher { headers: { Accept: 'application/json', 'Content-Type': 'application/json', - ...(credential && { Authorization: `Bearer ${credential}` }), + ...(credential.type === 'bearer token' && { + Authorization: `Bearer ${credential.token}`, + }), }, }; diff --git a/plugins/kubernetes-backend/src/service/KubernetesProxy.test.ts b/plugins/kubernetes-backend/src/service/KubernetesProxy.test.ts index 9c3e7e9187..70d639a6e8 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesProxy.test.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesProxy.test.ts @@ -23,6 +23,7 @@ import { AuthorizeResult, PermissionEvaluator, } from '@backstage/plugin-permission-common'; +import { KubernetesRequestAuth } from '@backstage/plugin-kubernetes-common'; import { getMockReq, getMockRes } from '@jest-mock/express'; import express from 'express'; import Router from 'express-promise-router'; @@ -33,7 +34,11 @@ import request from 'supertest'; import { AddressInfo, WebSocket, WebSocketServer } from 'ws'; import { LocalKubectlProxyClusterLocator } from '../cluster-locator/LocalKubectlProxyLocator'; -import { AuthenticationStrategy, AnonymousStrategy } from '../auth'; +import { + AuthenticationStrategy, + AnonymousStrategy, + KubernetesCredential, +} from '../auth'; import { ClusterDetails, KubernetesClustersSupplier } from '../types/types'; import { APPLICATION_JSON, @@ -47,6 +52,7 @@ import type { Request } from 'express'; describe('KubernetesProxy', () => { let proxy: KubernetesProxy; + let authStrategy: jest.Mocked; const worker = setupServer(); const logger = getVoidLogger(); @@ -59,11 +65,6 @@ describe('KubernetesProxy', () => { authorizeConditional: jest.fn(), }; - const authStrategy: jest.Mocked = { - getCredential: jest.fn(), - validate: jest.fn(), - }; - setupRequestMockHandlers(worker); const buildMockRequest = (clusterName: any, path: string): Request => { @@ -124,6 +125,15 @@ describe('KubernetesProxy', () => { beforeEach(() => { jest.resetAllMocks(); + authStrategy = { + getCredential: jest + .fn< + Promise, + [ClusterDetails, KubernetesRequestAuth] + >() + .mockResolvedValue({ type: 'anonymous' }), + validate: jest.fn(), + }; proxy = new KubernetesProxy({ logger, clusterSupplier, authStrategy }); permissionApi.authorize.mockResolvedValue([ { result: AuthorizeResult.ALLOW }, @@ -325,7 +335,10 @@ describe('KubernetesProxy', () => { }, ]); - authStrategy.getCredential.mockResolvedValue('strategy-provided-token'); + authStrategy.getCredential.mockResolvedValue({ + type: 'bearer token', + token: 'strategy-provided-token', + }); const requestPromise = setupProxyPromise({ proxyPath: '/mountpath', @@ -369,7 +382,10 @@ describe('KubernetesProxy', () => { }, ]); - authStrategy.getCredential.mockResolvedValue('my-token'); + authStrategy.getCredential.mockResolvedValue({ + type: 'bearer token', + token: 'my-token', + }); const requestPromise = setupProxyPromise({ proxyPath: '/mountpath', @@ -418,7 +434,10 @@ describe('KubernetesProxy', () => { }, ]); - authStrategy.getCredential.mockResolvedValue('tokenA'); + authStrategy.getCredential.mockResolvedValue({ + type: 'bearer token', + token: 'tokenA', + }); const requestPromise = setupProxyPromise({ proxyPath: '/mountpath', @@ -691,7 +710,7 @@ describe('KubernetesProxy', () => { event: 'connection' | 'open' | 'close' | 'error' | 'message', ) => new Promise(resolve => ws.once(event, x => resolve(x?.toString()))); - beforeAll(async () => { + beforeEach(async () => { await new Promise(resolve => { expressServer = express() .use( @@ -722,7 +741,7 @@ describe('KubernetesProxy', () => { wsEchoServer.on('error', console.error); }); - afterAll(() => { + afterEach(() => { wsEchoServer.close(); expressServer.close(); }); diff --git a/plugins/kubernetes-backend/src/service/KubernetesProxy.ts b/plugins/kubernetes-backend/src/service/KubernetesProxy.ts index ce96659c30..6e699d1220 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesProxy.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesProxy.ts @@ -112,11 +112,11 @@ export class KubernetesProxy { if (authHeader) { req.headers.authorization = authHeader; } else { - const serviceAccountToken = await this.getClusterForRequest(req).then( - cd => this.authStrategy.getCredential(cd, {}), - ); - if (serviceAccountToken) { - req.headers.authorization = `Bearer ${serviceAccountToken}`; + const credential = await this.getClusterForRequest(req).then(cd => { + return this.authStrategy.getCredential(cd, {}); + }); + if (credential.type === 'bearer token') { + req.headers.authorization = `Bearer ${credential.token}`; } } From e30bd8709d43d22315f41aa81a0b5d18a80bf0f5 Mon Sep 17 00:00:00 2001 From: Jamie Klassen Date: Mon, 11 Sep 2023 15:01:39 -0400 Subject: [PATCH 17/20] 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({ From 5d377c9b39fd0766f4bff810fbfcee8653ddeb37 Mon Sep 17 00:00:00 2001 From: Jamie Klassen Date: Mon, 11 Sep 2023 17:07:11 -0400 Subject: [PATCH 18/20] integrators can add strategies Due to the loosened wire format, now integrators have the opportunity to define arbitrary contracts between the front- and back-end when dealing with customized Kubernetes auth setups. Signed-off-by: Jamie Klassen --- plugins/kubernetes-backend/api-report.md | 2 + .../src/auth/AksStrategy.ts | 4 +- .../src/auth/GoogleStrategy.ts | 2 +- .../src/auth/OidcStrategy.ts | 5 +- .../src/service/KubernetesBuilder.test.ts | 132 +++++++++++++++++- .../src/service/KubernetesBuilder.ts | 5 + plugins/kubernetes-common/api-report.md | 11 +- plugins/kubernetes-common/src/types.ts | 8 +- plugins/kubernetes/package.json | 1 + .../KubernetesAuthProviders.test.ts | 2 +- .../OidcKubernetesAuthProvider.ts | 7 +- yarn.lock | 1 + 12 files changed, 149 insertions(+), 31 deletions(-) diff --git a/plugins/kubernetes-backend/api-report.md b/plugins/kubernetes-backend/api-report.md index 5aa124394a..603f15adeb 100644 --- a/plugins/kubernetes-backend/api-report.md +++ b/plugins/kubernetes-backend/api-report.md @@ -166,6 +166,8 @@ export const HEADER_KUBERNETES_CLUSTER: string; export class KubernetesBuilder { constructor(env: KubernetesEnvironment); // (undocumented) + addAuthStrategy(key: string, strategy: AuthenticationStrategy): this; + // (undocumented) build(): KubernetesBuilderReturn; // (undocumented) protected buildAuthStrategyMap(): { diff --git a/plugins/kubernetes-backend/src/auth/AksStrategy.ts b/plugins/kubernetes-backend/src/auth/AksStrategy.ts index db4d423060..999dd9fa11 100644 --- a/plugins/kubernetes-backend/src/auth/AksStrategy.ts +++ b/plugins/kubernetes-backend/src/auth/AksStrategy.ts @@ -27,7 +27,9 @@ export class AksStrategy implements AuthenticationStrategy { requestAuth: KubernetesRequestAuth, ): Promise { const token = requestAuth.aks; - return token ? { type: 'bearer token', token } : { type: 'anonymous' }; + return token + ? { type: 'bearer token', token: token as string } + : { type: 'anonymous' }; } public validate(_: AuthMetadata) {} } diff --git a/plugins/kubernetes-backend/src/auth/GoogleStrategy.ts b/plugins/kubernetes-backend/src/auth/GoogleStrategy.ts index ff8d02cb14..850cd24ba8 100644 --- a/plugins/kubernetes-backend/src/auth/GoogleStrategy.ts +++ b/plugins/kubernetes-backend/src/auth/GoogleStrategy.ts @@ -33,7 +33,7 @@ export class GoogleStrategy implements AuthenticationStrategy { 'Google token not found under auth.google in request body', ); } - return { type: 'bearer token', token }; + return { type: 'bearer token', token: token as string }; } public validate(_: AuthMetadata) {} } diff --git a/plugins/kubernetes-backend/src/auth/OidcStrategy.ts b/plugins/kubernetes-backend/src/auth/OidcStrategy.ts index f5ab1cbd8d..2d9a4808e5 100644 --- a/plugins/kubernetes-backend/src/auth/OidcStrategy.ts +++ b/plugins/kubernetes-backend/src/auth/OidcStrategy.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { JsonObject } from '@backstage/types'; import { ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER, KubernetesRequestAuth, @@ -38,14 +39,14 @@ export class OidcStrategy implements AuthenticationStrategy { ); } - const token = authConfig.oidc?.[oidcTokenProvider]; + const token = (authConfig.oidc as JsonObject | null)?.[oidcTokenProvider]; if (!token) { throw new Error( `Auth token not found under oidc.${oidcTokenProvider} in request body`, ); } - return { type: 'bearer token', token }; + return { type: 'bearer token', token: token as string }; } public validate(authMetadata: AuthMetadata) { diff --git a/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts b/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts index dbcb0659b7..e7e1e07549 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts @@ -21,6 +21,7 @@ import { ANNOTATION_KUBERNETES_AUTH_PROVIDER, ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER, ObjectsByEntityResponse, + KubernetesRequestAuth, } from '@backstage/plugin-kubernetes-common'; import express from 'express'; import request from 'supertest'; @@ -55,7 +56,8 @@ describe('KubernetesBuilder', () => { let catalogApi: CatalogApi; let permissions: jest.Mocked; - beforeAll(async () => { + beforeEach(async () => { + jest.resetAllMocks(); const logger = getVoidLogger(); config = new ConfigReader({ kubernetes: { @@ -109,10 +111,6 @@ describe('KubernetesBuilder', () => { app = express().use(router); }); - beforeEach(() => { - jest.resetAllMocks(); - }); - describe('get /clusters', () => { it('happy path: lists clusters', async () => { const response = await request(app).get('/clusters'); @@ -298,8 +296,77 @@ describe('KubernetesBuilder', () => { expect(response.body).toEqual(result); expect(response.status).toEqual(200); }); + + it('reads auth data for custom strategy', async () => { + permissions.authorize.mockResolvedValue([ + { result: AuthorizeResult.ALLOW }, + ]); + const mockFetcher = { + fetchPodMetricsByNamespaces: jest + .fn() + .mockResolvedValue({ errors: [], responses: [] }), + fetchObjectsForService: jest.fn().mockResolvedValue({ + errors: [], + responses: [ + { type: 'pods', resources: [{ metadata: { name: 'pod1' } }] }, + ], + }), + }; + const { router } = await KubernetesBuilder.createBuilder({ + logger: getVoidLogger(), + config, + catalogApi, + permissions, + }) + .addAuthStrategy('custom', { + getCredential: jest + .fn< + Promise, + [ClusterDetails, KubernetesRequestAuth] + >() + .mockImplementation(async (_, requestAuth) => ({ + type: 'bearer token', + token: requestAuth.custom as string, + })), + validate: jest.fn(), + }) + .setClusterSupplier({ + getClusters: jest + .fn, []>() + .mockResolvedValue([ + { + name: 'custom-cluster', + url: 'http://my.cluster.url', + authMetadata: { + [ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'custom', + }, + }, + ]), + }) + .setFetcher(mockFetcher) + .build(); + app = express().use(router); + + await request(app) + .post('/services/test-service') + .send({ + entity: { + metadata: { + name: 'thing', + }, + }, + auth: { custom: 'custom-token' }, + }); + + expect(mockFetcher.fetchObjectsForService).toHaveBeenCalledWith( + expect.objectContaining({ + credential: { type: 'bearer token', token: 'custom-token' }, + }), + ); + }); }); - describe('post /proxy', () => { + + describe('/proxy', () => { const worker = setupServer(); setupRequestMockHandlers(worker); @@ -400,6 +467,59 @@ metadata: expect(response.status).toEqual(403); }); + + it('permits custom client-side auth strategy', async () => { + worker.use( + rest.get('http://my.cluster.url/api/v1/namespaces', (req, res, ctx) => { + if (req.headers.get('Authorization') !== 'custom-token') { + return res(ctx.status(401)); + } + return res(ctx.json({ items: [] })); + }), + ); + permissions.authorize.mockResolvedValue([ + { result: AuthorizeResult.ALLOW }, + ]); + const { router } = await KubernetesBuilder.createBuilder({ + logger: getVoidLogger(), + config, + catalogApi, + permissions, + }) + .addAuthStrategy('custom', { + getCredential: jest + .fn< + Promise, + [ClusterDetails, KubernetesRequestAuth] + >() + .mockResolvedValue({ type: 'anonymous' }), + validate: jest.fn(), + }) + .setClusterSupplier({ + getClusters: jest + .fn, []>() + .mockResolvedValue([ + { + name: 'custom-cluster', + url: 'http://my.cluster.url', + authMetadata: { + [ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'custom', + }, + }, + ]), + }) + .build(); + app = express().use(router); + + const proxyEndpointRequest = request(app) + .get('/proxy/api/v1/namespaces') + .set(HEADER_KUBERNETES_CLUSTER, 'custom-cluster') + .set(HEADER_KUBERNETES_AUTH, 'custom-token'); + worker.use(rest.all(proxyEndpointRequest.url, req => req.passthrough())); + const response = await proxyEndpointRequest; + + expect(response.body).toStrictEqual({ items: [] }); + }); }); describe('get /.well-known/backstage/permissions/metadata', () => { it('lists permissions supported by the kubernetes plugin', async () => { diff --git a/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts b/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts index f3e71e8dbc..f3766d8513 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts @@ -204,6 +204,11 @@ export class KubernetesBuilder { this.authStrategyMap = authStrategyMap; } + public addAuthStrategy(key: string, strategy: AuthenticationStrategy) { + this.getAuthStrategyMap()[key] = strategy; + return this; + } + protected buildCustomResources() { const customResources: CustomResource[] = ( this.env.config.getOptionalConfigArray('kubernetes.customResources') ?? [] diff --git a/plugins/kubernetes-common/api-report.md b/plugins/kubernetes-common/api-report.md index aae72d6f1e..051293f37f 100644 --- a/plugins/kubernetes-common/api-report.md +++ b/plugins/kubernetes-common/api-report.md @@ -233,16 +233,7 @@ export const kubernetesPermissions: BasicPermission[]; export const kubernetesProxyPermission: BasicPermission; // @public (undocumented) -export interface KubernetesRequestAuth { - // (undocumented) - aks?: string; - // (undocumented) - google?: string; - // (undocumented) - oidc?: { - [key: string]: string; - }; -} +export type KubernetesRequestAuth = JsonObject; // @public (undocumented) export interface KubernetesRequestBody { diff --git a/plugins/kubernetes-common/src/types.ts b/plugins/kubernetes-common/src/types.ts index 2b2ab05b51..feb213d520 100644 --- a/plugins/kubernetes-common/src/types.ts +++ b/plugins/kubernetes-common/src/types.ts @@ -33,13 +33,7 @@ import { import { Entity } from '@backstage/catalog-model'; /** @public */ -export interface KubernetesRequestAuth { - google?: string; - aks?: string; - oidc?: { - [key: string]: string; - }; -} +export type KubernetesRequestAuth = JsonObject; /** @public */ export interface CustomResourceMatcher { diff --git a/plugins/kubernetes/package.json b/plugins/kubernetes/package.json index 7a0c2bf929..4bd55d65a3 100644 --- a/plugins/kubernetes/package.json +++ b/plugins/kubernetes/package.json @@ -42,6 +42,7 @@ "@backstage/plugin-catalog-react": "workspace:^", "@backstage/plugin-kubernetes-common": "workspace:^", "@backstage/theme": "workspace:^", + "@backstage/types": "workspace:^", "@kubernetes-models/apimachinery": "^1.1.0", "@kubernetes-models/base": "^4.0.1", "@kubernetes/client-node": "0.18.1", diff --git a/plugins/kubernetes/src/kubernetes-auth-provider/KubernetesAuthProviders.test.ts b/plugins/kubernetes/src/kubernetes-auth-provider/KubernetesAuthProviders.test.ts index 14e574022a..5d03dcdae3 100644 --- a/plugins/kubernetes/src/kubernetes-auth-provider/KubernetesAuthProviders.test.ts +++ b/plugins/kubernetes/src/kubernetes-auth-provider/KubernetesAuthProviders.test.ts @@ -74,7 +74,7 @@ describe('KubernetesAuthProviders tests', () => { requestBody, ); - expect(details.auth?.oidc?.okta).toBe('oktaToken'); + expect(details).toMatchObject({ auth: { oidc: { okta: 'oktaToken' } } }); }); it('returns error for unknown authProvider', async () => { diff --git a/plugins/kubernetes/src/kubernetes-auth-provider/OidcKubernetesAuthProvider.ts b/plugins/kubernetes/src/kubernetes-auth-provider/OidcKubernetesAuthProvider.ts index bd5760bde2..b2e6c5cbc2 100644 --- a/plugins/kubernetes/src/kubernetes-auth-provider/OidcKubernetesAuthProvider.ts +++ b/plugins/kubernetes/src/kubernetes-auth-provider/OidcKubernetesAuthProvider.ts @@ -14,9 +14,10 @@ * limitations under the License. */ -import { KubernetesAuthProvider } from './types'; +import { JsonObject } from '@backstage/types'; import { KubernetesRequestBody } from '@backstage/plugin-kubernetes-common'; import { OpenIdConnectApi } from '@backstage/core-plugin-api'; +import { KubernetesAuthProvider } from './types'; export class OidcKubernetesAuthProvider implements KubernetesAuthProvider { providerName: string; @@ -31,9 +32,9 @@ export class OidcKubernetesAuthProvider implements KubernetesAuthProvider { requestBody: KubernetesRequestBody, ): Promise { const authToken: string = (await this.getCredentials()).token; - const auth = { ...requestBody.auth }; + const auth = { ...(requestBody.auth as JsonObject) }; if (auth.oidc) { - auth.oidc[this.providerName] = authToken; + (auth.oidc as JsonObject)[this.providerName] = authToken; } else { auth.oidc = { [this.providerName]: authToken }; } diff --git a/yarn.lock b/yarn.lock index 78d55e945d..982538afb4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7693,6 +7693,7 @@ __metadata: "@backstage/plugin-kubernetes-common": "workspace:^" "@backstage/test-utils": "workspace:^" "@backstage/theme": "workspace:^" + "@backstage/types": "workspace:^" "@kubernetes-models/apimachinery": ^1.1.0 "@kubernetes-models/base": ^4.0.1 "@kubernetes/client-node": 0.18.1 From 0ad36158d980fe241fb257fe2367252591bde2dc Mon Sep 17 00:00:00 2001 From: Jamie Klassen Date: Mon, 11 Sep 2023 18:30:46 -0400 Subject: [PATCH 19/20] changeset Signed-off-by: Jamie Klassen --- .changeset/afraid-apes-smell.md | 6 ++++++ .changeset/new-frogs-punch.md | 7 +++++++ 2 files changed, 13 insertions(+) create mode 100644 .changeset/afraid-apes-smell.md create mode 100644 .changeset/new-frogs-punch.md diff --git a/.changeset/afraid-apes-smell.md b/.changeset/afraid-apes-smell.md new file mode 100644 index 0000000000..2654351ad8 --- /dev/null +++ b/.changeset/afraid-apes-smell.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-kubernetes': patch +'@backstage/plugin-kubernetes-common': patch +--- + +Loosened the type of the `auth` field in the body of requests to the `retrieveObjectsByServiceId` endpoint. Now any JSON object is allowed, which should make it easier for integrators to write their own custom auth strategies for Kubernetes. diff --git a/.changeset/new-frogs-punch.md b/.changeset/new-frogs-punch.md new file mode 100644 index 0000000000..f4390e72c6 --- /dev/null +++ b/.changeset/new-frogs-punch.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-kubernetes-backend': minor +--- + +Integrators can now bring their own auth strategies through the use of the `addAuthStrategy` method on `KubernetesBuilder`. + +**BREAKING** on the slight chance you were using the `setAuthTranslatorMap` method on `KubernetesBuilder`, it has been removed along with the entire `KubernetesAuthTranslator` interface. This notion has been replaced with the more focused concept of an `AuthenticationStrategy`. Converting a translator to a strategy should not be especially difficult. From 9212955b35b6afac24231385fe1380c163b8bf70 Mon Sep 17 00:00:00 2001 From: Jamie Klassen Date: Thu, 14 Sep 2023 17:51:35 -0400 Subject: [PATCH 20/20] better name + signature for validation method Signed-off-by: Jamie Klassen --- plugins/kubernetes-backend/api-report.md | 20 +++++++++---------- .../src/auth/AksStrategy.ts | 6 ++++-- .../src/auth/AnonymousStrategy.ts | 4 +++- .../src/auth/AwsIamStrategy.ts | 4 +++- .../src/auth/AzureIdentityStrategy.ts | 4 +++- .../src/auth/DispatchStrategy.test.ts | 2 +- .../src/auth/DispatchStrategy.ts | 12 ++++++----- .../src/auth/GoogleServiceAccountStrategy.ts | 4 +++- .../src/auth/GoogleStrategy.ts | 6 ++++-- .../src/auth/OidcStrategy.test.ts | 6 +++--- .../src/auth/OidcStrategy.ts | 5 +++-- .../src/auth/ServiceAccountStrategy.ts | 4 +++- plugins/kubernetes-backend/src/auth/types.ts | 2 +- .../ConfigClusterLocator.test.ts | 6 ++---- .../cluster-locator/ConfigClusterLocator.ts | 11 ++++++---- .../src/cluster-locator/index.test.ts | 2 +- .../src/service/KubernetesBuilder.test.ts | 4 ++-- .../service/KubernetesFanOutHandler.test.ts | 4 ++-- .../src/service/KubernetesProxy.test.ts | 2 +- 19 files changed, 63 insertions(+), 45 deletions(-) diff --git a/plugins/kubernetes-backend/api-report.md b/plugins/kubernetes-backend/api-report.md index 603f15adeb..051706bbd5 100644 --- a/plugins/kubernetes-backend/api-report.md +++ b/plugins/kubernetes-backend/api-report.md @@ -29,7 +29,7 @@ export class AksStrategy implements AuthenticationStrategy { requestAuth: KubernetesRequestAuth, ): Promise; // (undocumented) - validate(_: AuthMetadata): void; + validateCluster(): Error[]; } // @public (undocumented) @@ -37,7 +37,7 @@ export class AnonymousStrategy implements AuthenticationStrategy { // (undocumented) getCredential(): Promise; // (undocumented) - validate(): void; + validateCluster(): Error[]; } // @public (undocumented) @@ -48,7 +48,7 @@ export interface AuthenticationStrategy { authConfig: KubernetesRequestAuth, ): Promise; // (undocumented) - validate(authMetadata: AuthMetadata): void; + validateCluster(authMetadata: AuthMetadata): Error[]; } // @public @@ -60,7 +60,7 @@ export class AwsIamStrategy implements AuthenticationStrategy { // (undocumented) getCredential(clusterDetails: ClusterDetails): Promise; // (undocumented) - validate(): void; + validateCluster(): Error[]; } // @public (undocumented) @@ -69,7 +69,7 @@ export class AzureIdentityStrategy implements AuthenticationStrategy { // (undocumented) getCredential(): Promise; // (undocumented) - validate(): void; + validateCluster(): Error[]; } // @public (undocumented) @@ -119,7 +119,7 @@ export class DispatchStrategy implements AuthenticationStrategy { auth: KubernetesRequestAuth, ): Promise; // (undocumented) - validate(authMetadata: AuthMetadata): void; + validateCluster(authMetadata: AuthMetadata): Error[]; } // @public (undocumented) @@ -142,7 +142,7 @@ export class GoogleServiceAccountStrategy implements AuthenticationStrategy { // (undocumented) getCredential(): Promise; // (undocumented) - validate(): void; + validateCluster(): Error[]; } // @public (undocumented) @@ -153,7 +153,7 @@ export class GoogleStrategy implements AuthenticationStrategy { requestAuth: KubernetesRequestAuth, ): Promise; // (undocumented) - validate(_: AuthMetadata): void; + validateCluster(): Error[]; } // @public @@ -441,7 +441,7 @@ export class OidcStrategy implements AuthenticationStrategy { authConfig: KubernetesRequestAuth, ): Promise; // (undocumented) - validate(authMetadata: AuthMetadata): void; + validateCluster(authMetadata: AuthMetadata): Error[]; } // @public (undocumented) @@ -465,7 +465,7 @@ export class ServiceAccountStrategy implements AuthenticationStrategy { // (undocumented) getCredential(clusterDetails: ClusterDetails): Promise; // (undocumented) - validate(): void; + validateCluster(): Error[]; } // @public (undocumented) diff --git a/plugins/kubernetes-backend/src/auth/AksStrategy.ts b/plugins/kubernetes-backend/src/auth/AksStrategy.ts index 999dd9fa11..07bdf2efe0 100644 --- a/plugins/kubernetes-backend/src/auth/AksStrategy.ts +++ b/plugins/kubernetes-backend/src/auth/AksStrategy.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { AuthMetadata, ClusterDetails } from '../types/types'; +import { ClusterDetails } from '../types/types'; import { AuthenticationStrategy, KubernetesCredential } from './types'; import { KubernetesRequestAuth } from '@backstage/plugin-kubernetes-common'; @@ -31,5 +31,7 @@ export class AksStrategy implements AuthenticationStrategy { ? { type: 'bearer token', token: token as string } : { type: 'anonymous' }; } - public validate(_: AuthMetadata) {} + public validateCluster(): Error[] { + return []; + } } diff --git a/plugins/kubernetes-backend/src/auth/AnonymousStrategy.ts b/plugins/kubernetes-backend/src/auth/AnonymousStrategy.ts index 20e688c2a3..30a9593773 100644 --- a/plugins/kubernetes-backend/src/auth/AnonymousStrategy.ts +++ b/plugins/kubernetes-backend/src/auth/AnonymousStrategy.ts @@ -25,5 +25,7 @@ export class AnonymousStrategy implements AuthenticationStrategy { return { type: 'anonymous' }; } - public validate() {} + public validateCluster(): Error[] { + return []; + } } diff --git a/plugins/kubernetes-backend/src/auth/AwsIamStrategy.ts b/plugins/kubernetes-backend/src/auth/AwsIamStrategy.ts index 90ea3ba1b2..53b880d60c 100644 --- a/plugins/kubernetes-backend/src/auth/AwsIamStrategy.ts +++ b/plugins/kubernetes-backend/src/auth/AwsIamStrategy.ts @@ -64,7 +64,9 @@ export class AwsIamStrategy implements AuthenticationStrategy { }; } - public validate() {} + public validateCluster(): Error[] { + return []; + } private async getBearerToken( clusterName: string, diff --git a/plugins/kubernetes-backend/src/auth/AzureIdentityStrategy.ts b/plugins/kubernetes-backend/src/auth/AzureIdentityStrategy.ts index cca0f3dede..d0eea3c13c 100644 --- a/plugins/kubernetes-backend/src/auth/AzureIdentityStrategy.ts +++ b/plugins/kubernetes-backend/src/auth/AzureIdentityStrategy.ts @@ -51,7 +51,9 @@ export class AzureIdentityStrategy implements AuthenticationStrategy { : { type: 'anonymous' }; } - public validate() {} + public validateCluster(): Error[] { + return []; + } private async fetchNewToken(): Promise { try { diff --git a/plugins/kubernetes-backend/src/auth/DispatchStrategy.test.ts b/plugins/kubernetes-backend/src/auth/DispatchStrategy.test.ts index f7db6ccd95..7202311d6f 100644 --- a/plugins/kubernetes-backend/src/auth/DispatchStrategy.test.ts +++ b/plugins/kubernetes-backend/src/auth/DispatchStrategy.test.ts @@ -30,7 +30,7 @@ describe('getCredential', () => { beforeEach(() => { mockStrategy = { getCredential: jest.fn(), - validate: jest.fn(), + validateCluster: jest.fn(), }; strategy = new DispatchStrategy({ authStrategyMap: { google: mockStrategy }, diff --git a/plugins/kubernetes-backend/src/auth/DispatchStrategy.ts b/plugins/kubernetes-backend/src/auth/DispatchStrategy.ts index b16340f8bc..f12e49b577 100644 --- a/plugins/kubernetes-backend/src/auth/DispatchStrategy.ts +++ b/plugins/kubernetes-backend/src/auth/DispatchStrategy.ts @@ -55,14 +55,16 @@ export class DispatchStrategy implements AuthenticationStrategy { ); } - public validate(authMetadata: AuthMetadata) { + public validateCluster(authMetadata: AuthMetadata): Error[] { const authProvider = authMetadata[ANNOTATION_KUBERNETES_AUTH_PROVIDER]; const strategy = this.strategyMap[authProvider]; if (!strategy) { - throw new Error( - `authProvider "${authProvider}" has no config associated with it`, - ); + return [ + new Error( + `authProvider "${authProvider}" has no config associated with it`, + ), + ]; } - strategy.validate(authMetadata); + return strategy.validateCluster(authMetadata); } } diff --git a/plugins/kubernetes-backend/src/auth/GoogleServiceAccountStrategy.ts b/plugins/kubernetes-backend/src/auth/GoogleServiceAccountStrategy.ts index 4aeba504af..2a44a31d6c 100644 --- a/plugins/kubernetes-backend/src/auth/GoogleServiceAccountStrategy.ts +++ b/plugins/kubernetes-backend/src/auth/GoogleServiceAccountStrategy.ts @@ -33,5 +33,7 @@ export class GoogleServiceAccountStrategy implements AuthenticationStrategy { return { type: 'bearer token', token }; } - public validate() {} + public validateCluster(): Error[] { + return []; + } } diff --git a/plugins/kubernetes-backend/src/auth/GoogleStrategy.ts b/plugins/kubernetes-backend/src/auth/GoogleStrategy.ts index 850cd24ba8..c1290dc1fe 100644 --- a/plugins/kubernetes-backend/src/auth/GoogleStrategy.ts +++ b/plugins/kubernetes-backend/src/auth/GoogleStrategy.ts @@ -15,7 +15,7 @@ */ import { AuthenticationStrategy, KubernetesCredential } from './types'; -import { AuthMetadata, ClusterDetails } from '../types/types'; +import { ClusterDetails } from '../types/types'; import { KubernetesRequestAuth } from '@backstage/plugin-kubernetes-common'; /** @@ -35,5 +35,7 @@ export class GoogleStrategy implements AuthenticationStrategy { } return { type: 'bearer token', token: token as string }; } - public validate(_: AuthMetadata) {} + public validateCluster(): Error[] { + return []; + } } diff --git a/plugins/kubernetes-backend/src/auth/OidcStrategy.test.ts b/plugins/kubernetes-backend/src/auth/OidcStrategy.test.ts index fe25b03c72..46213d9f3e 100644 --- a/plugins/kubernetes-backend/src/auth/OidcStrategy.test.ts +++ b/plugins/kubernetes-backend/src/auth/OidcStrategy.test.ts @@ -75,10 +75,10 @@ describe('OidcStrategy', () => { }); }); - describe('validate', () => { + describe('validateCluster', () => { it('fails when token provider is not specified', () => { - expect(() => strategy.validate({})).toThrow( - `Must specify a token provider for 'oidc' strategy`, + expect(strategy.validateCluster({})).toContainEqual( + new Error(`Must specify a token provider for 'oidc' strategy`), ); }); }); diff --git a/plugins/kubernetes-backend/src/auth/OidcStrategy.ts b/plugins/kubernetes-backend/src/auth/OidcStrategy.ts index 2d9a4808e5..445799cbb8 100644 --- a/plugins/kubernetes-backend/src/auth/OidcStrategy.ts +++ b/plugins/kubernetes-backend/src/auth/OidcStrategy.ts @@ -49,11 +49,12 @@ export class OidcStrategy implements AuthenticationStrategy { return { type: 'bearer token', token: token as string }; } - public validate(authMetadata: AuthMetadata) { + public validateCluster(authMetadata: AuthMetadata): Error[] { const oidcTokenProvider = authMetadata[ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER]; if (!oidcTokenProvider || oidcTokenProvider === '') { - throw new Error(`Must specify a token provider for 'oidc' strategy`); + return [new Error(`Must specify a token provider for 'oidc' strategy`)]; } + return []; } } diff --git a/plugins/kubernetes-backend/src/auth/ServiceAccountStrategy.ts b/plugins/kubernetes-backend/src/auth/ServiceAccountStrategy.ts index 9658b1d8eb..714b2a94d1 100644 --- a/plugins/kubernetes-backend/src/auth/ServiceAccountStrategy.ts +++ b/plugins/kubernetes-backend/src/auth/ServiceAccountStrategy.ts @@ -42,5 +42,7 @@ export class ServiceAccountStrategy implements AuthenticationStrategy { }; } - public validate() {} + public validateCluster(): Error[] { + return []; + } } diff --git a/plugins/kubernetes-backend/src/auth/types.ts b/plugins/kubernetes-backend/src/auth/types.ts index 4094f37c3b..fff9952bae 100644 --- a/plugins/kubernetes-backend/src/auth/types.ts +++ b/plugins/kubernetes-backend/src/auth/types.ts @@ -34,5 +34,5 @@ export interface AuthenticationStrategy { clusterDetails: ClusterDetails, authConfig: KubernetesRequestAuth, ): Promise; - validate(authMetadata: AuthMetadata): void; + validateCluster(authMetadata: AuthMetadata): Error[]; } diff --git a/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.test.ts b/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.test.ts index e48752bbd0..258cddcd66 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.test.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.test.ts @@ -31,7 +31,7 @@ describe('ConfigClusterLocator', () => { beforeEach(() => { authStrategy = { getCredential: jest.fn(), - validate: jest.fn(), + validateCluster: jest.fn().mockReturnValue([]), }; }); @@ -307,9 +307,7 @@ describe('ConfigClusterLocator', () => { }, ], }); - authStrategy.validate.mockImplementation(_ => { - throw new Error('mock error'); - }); + authStrategy.validateCluster.mockReturnValue([new Error('mock error')]); expect(() => ConfigClusterLocator.fromConfig(config, authStrategy)).toThrow( `Invalid cluster 'cluster1': mock error`, diff --git a/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.ts b/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.ts index e321d001d3..7899d7762d 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.ts @@ -74,11 +74,14 @@ export class ConfigClusterLocator implements KubernetesClustersSupplier { clusterDetails.dashboardParameters = c.get('dashboardParameters'); } - try { - authStrategy.validate(clusterDetails.authMetadata); - } catch (e) { + const validationErrors = authStrategy.validateCluster( + clusterDetails.authMetadata, + ); + if (validationErrors.length !== 0) { throw new Error( - `Invalid cluster '${clusterDetails.name}': ${e.message}`, + `Invalid cluster '${clusterDetails.name}': ${validationErrors + .map(e => e.message) + .join(', ')}`, ); } return clusterDetails; diff --git a/plugins/kubernetes-backend/src/cluster-locator/index.test.ts b/plugins/kubernetes-backend/src/cluster-locator/index.test.ts index e7a58b3f98..ded0faac5b 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/index.test.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/index.test.ts @@ -52,7 +52,7 @@ describe('getCombinedClusterSupplier', () => { ); const mockStrategy: jest.Mocked = { getCredential: jest.fn(), - validate: jest.fn(), + validateCluster: jest.fn().mockReturnValue([]), }; const clusterSupplier = getCombinedClusterSupplier( diff --git a/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts b/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts index e7e1e07549..85a4326d60 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts @@ -328,7 +328,7 @@ describe('KubernetesBuilder', () => { type: 'bearer token', token: requestAuth.custom as string, })), - validate: jest.fn(), + validateCluster: jest.fn().mockReturnValue([]), }) .setClusterSupplier({ getClusters: jest @@ -493,7 +493,7 @@ metadata: [ClusterDetails, KubernetesRequestAuth] >() .mockResolvedValue({ type: 'anonymous' }), - validate: jest.fn(), + validateCluster: jest.fn().mockReturnValue([]), }) .setClusterSupplier({ getClusters: jest diff --git a/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.test.ts b/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.test.ts index 75283a34f7..1faf2f0673 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.test.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.test.ts @@ -195,7 +195,7 @@ describe('KubernetesFanOutHandler', () => { [ClusterDetails, KubernetesRequestAuth] >() .mockResolvedValue({ type: 'anonymous' }), - validate: jest.fn(), + validateCluster: jest.fn().mockReturnValue([]), }, config, }); @@ -1146,7 +1146,7 @@ describe('KubernetesFanOutHandler', () => { [ClusterDetails, KubernetesRequestAuth] >() .mockResolvedValue({ type: 'bearer token', token: 'token' }), - validate: jest.fn(), + validateCluster: jest.fn().mockReturnValue([]), }, config, }); diff --git a/plugins/kubernetes-backend/src/service/KubernetesProxy.test.ts b/plugins/kubernetes-backend/src/service/KubernetesProxy.test.ts index 70d639a6e8..a84482082f 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesProxy.test.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesProxy.test.ts @@ -132,7 +132,7 @@ describe('KubernetesProxy', () => { [ClusterDetails, KubernetesRequestAuth] >() .mockResolvedValue({ type: 'anonymous' }), - validate: jest.fn(), + validateCluster: jest.fn(), }; proxy = new KubernetesProxy({ logger, clusterSupplier, authStrategy }); permissionApi.authorize.mockResolvedValue([