From ce6905f86df53bc304532ad7a483b9a8885c5f05 Mon Sep 17 00:00:00 2001 From: Jamie Klassen Date: Wed, 6 Sep 2023 19:55:46 -0400 Subject: [PATCH] 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.