diff --git a/.changeset/kind-clouds-fly.md b/.changeset/kind-clouds-fly.md new file mode 100644 index 0000000000..7d8d7c4a0e --- /dev/null +++ b/.changeset/kind-clouds-fly.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-kubernetes-backend': patch +'@backstage/plugin-kubernetes-node': patch +--- + +Enabled a way to include custom auth metadata info on the clusters endpoint diff --git a/plugins/kubernetes-backend/api-report.md b/plugins/kubernetes-backend/api-report.md index e6996484dd..afdb3bcf9a 100644 --- a/plugins/kubernetes-backend/api-report.md +++ b/plugins/kubernetes-backend/api-report.md @@ -4,6 +4,7 @@ ```ts import { AuthenticationStrategy as AuthenticationStrategy_2 } from '@backstage/plugin-kubernetes-node'; +import { AuthMetadata as AuthMetadata_2 } from '@backstage/plugin-kubernetes-node'; import { CatalogApi } from '@backstage/catalog-client'; import { ClusterDetails as ClusterDetails_2 } from '@backstage/plugin-kubernetes-node'; import { Config } from '@backstage/config'; @@ -12,9 +13,12 @@ import { Duration } from 'luxon'; import express from 'express'; import * as k8sAuthTypes from '@backstage/plugin-kubernetes-node'; import { KubernetesClustersSupplier as KubernetesClustersSupplier_2 } from '@backstage/plugin-kubernetes-node'; +import { KubernetesCredential as KubernetesCredential_2 } from '@backstage/plugin-kubernetes-node'; +import { KubernetesFetcher as KubernetesFetcher_2 } from '@backstage/plugin-kubernetes-node'; import { KubernetesObjectsProvider as KubernetesObjectsProvider_2 } from '@backstage/plugin-kubernetes-node'; import { KubernetesRequestAuth } from '@backstage/plugin-kubernetes-common'; import type { KubernetesRequestBody } from '@backstage/plugin-kubernetes-common'; +import { KubernetesServiceLocator as KubernetesServiceLocator_2 } from '@backstage/plugin-kubernetes-node'; import { Logger } from 'winston'; import { ObjectToFetch as ObjectToFetch_2 } from '@backstage/plugin-kubernetes-node'; import { PermissionEvaluator } from '@backstage/plugin-permission-common'; @@ -23,20 +27,24 @@ import { RequestHandler } from 'http-proxy-middleware'; import { TokenCredential } from '@azure/identity'; // @public (undocumented) -export class AksStrategy implements AuthenticationStrategy { +export class AksStrategy implements AuthenticationStrategy_2 { // (undocumented) getCredential( - _: ClusterDetails, + _: ClusterDetails_2, requestAuth: KubernetesRequestAuth, - ): Promise; + ): Promise; + // (undocumented) + presentAuthMetadata(_authMetadata: AuthMetadata_2): AuthMetadata_2; // (undocumented) validateCluster(): Error[]; } // @public (undocumented) -export class AnonymousStrategy implements AuthenticationStrategy { +export class AnonymousStrategy implements AuthenticationStrategy_2 { // (undocumented) - getCredential(): Promise; + getCredential(): Promise; + // (undocumented) + presentAuthMetadata(_authMetadata: AuthMetadata_2): AuthMetadata_2; // (undocumented) validateCluster(): Error[]; } @@ -48,19 +56,25 @@ export type AuthenticationStrategy = k8sAuthTypes.AuthenticationStrategy; export type AuthMetadata = k8sAuthTypes.AuthMetadata; // @public (undocumented) -export class AwsIamStrategy implements AuthenticationStrategy { +export class AwsIamStrategy implements AuthenticationStrategy_2 { constructor(opts: { config: Config }); // (undocumented) - getCredential(clusterDetails: ClusterDetails): Promise; + getCredential( + clusterDetails: ClusterDetails_2, + ): Promise; + // (undocumented) + presentAuthMetadata(_authMetadata: AuthMetadata_2): AuthMetadata_2; // (undocumented) validateCluster(): Error[]; } // @public (undocumented) -export class AzureIdentityStrategy implements AuthenticationStrategy { +export class AzureIdentityStrategy implements AuthenticationStrategy_2 { constructor(logger: Logger, tokenCredential?: TokenCredential); // (undocumented) - getCredential(): Promise; + getCredential(): Promise; + // (undocumented) + presentAuthMetadata(_authMetadata: AuthMetadata_2): AuthMetadata_2; // (undocumented) validateCluster(): Error[]; } @@ -81,21 +95,23 @@ export type CustomResourcesByEntity = k8sAuthTypes.CustomResourcesByEntity; export const DEFAULT_OBJECTS: ObjectToFetch[]; // @public -export class DispatchStrategy implements AuthenticationStrategy { +export class DispatchStrategy implements AuthenticationStrategy_2 { constructor(options: DispatchStrategyOptions); // (undocumented) getCredential( - clusterDetails: ClusterDetails, + clusterDetails: ClusterDetails_2, auth: KubernetesRequestAuth, - ): Promise; + ): Promise; // (undocumented) - validateCluster(authMetadata: AuthMetadata): Error[]; + presentAuthMetadata(_authMetadata: AuthMetadata_2): AuthMetadata_2; + // (undocumented) + validateCluster(authMetadata: AuthMetadata_2): Error[]; } // @public (undocumented) export type DispatchStrategyOptions = { authStrategyMap: { - [key: string]: AuthenticationStrategy; + [key: string]: AuthenticationStrategy_2; }; }; @@ -103,20 +119,24 @@ export type DispatchStrategyOptions = { export type FetchResponseWrapper = k8sAuthTypes.FetchResponseWrapper; // @public (undocumented) -export class GoogleServiceAccountStrategy implements AuthenticationStrategy { +export class GoogleServiceAccountStrategy implements AuthenticationStrategy_2 { // (undocumented) - getCredential(): Promise; + getCredential(): Promise; + // (undocumented) + presentAuthMetadata(_authMetadata: AuthMetadata_2): AuthMetadata_2; // (undocumented) validateCluster(): Error[]; } // @public (undocumented) -export class GoogleStrategy implements AuthenticationStrategy { +export class GoogleStrategy implements AuthenticationStrategy_2 { // (undocumented) getCredential( - _: ClusterDetails, + _: ClusterDetails_2, requestAuth: KubernetesRequestAuth, - ): Promise; + ): Promise; + // (undocumented) + presentAuthMetadata(_authMetadata: AuthMetadata_2): AuthMetadata_2; // (undocumented) validateCluster(): Error[]; } @@ -131,7 +151,7 @@ export const HEADER_KUBERNETES_CLUSTER: string; export class KubernetesBuilder { constructor(env: KubernetesEnvironment); // (undocumented) - addAuthStrategy(key: string, strategy: AuthenticationStrategy): this; + addAuthStrategy(key: string, strategy: AuthenticationStrategy_2): this; // (undocumented) build(): KubernetesBuilderReturn; // (undocumented) @@ -145,15 +165,15 @@ export class KubernetesBuilder { // (undocumented) protected buildCustomResources(): CustomResource_2[]; // (undocumented) - protected buildFetcher(): KubernetesFetcher; + protected buildFetcher(): KubernetesFetcher_2; // (undocumented) protected buildHttpServiceLocator( _clusterSupplier: KubernetesClustersSupplier_2, - ): KubernetesServiceLocator; + ): KubernetesServiceLocator_2; // (undocumented) protected buildMultiTenantServiceLocator( clusterSupplier: KubernetesClustersSupplier_2, - ): KubernetesServiceLocator; + ): KubernetesServiceLocator_2; // (undocumented) protected buildObjectsProvider( options: KubernetesObjectsProviderOptions, @@ -175,11 +195,11 @@ export class KubernetesBuilder { protected buildServiceLocator( method: ServiceLocatorMethod, clusterSupplier: KubernetesClustersSupplier_2, - ): KubernetesServiceLocator; + ): KubernetesServiceLocator_2; // (undocumented) protected buildSingleTenantServiceLocator( clusterSupplier: KubernetesClustersSupplier_2, - ): KubernetesServiceLocator; + ): KubernetesServiceLocator_2; // (undocumented) static createBuilder(env: KubernetesEnvironment): KubernetesBuilder; // (undocumented) @@ -195,7 +215,7 @@ export class KubernetesBuilder { // (undocumented) protected getClusterSupplier(): KubernetesClustersSupplier_2; // (undocumented) - protected getFetcher(): KubernetesFetcher; + protected getFetcher(): KubernetesFetcher_2; // (undocumented) protected getObjectsProvider( options: KubernetesObjectsProviderOptions, @@ -208,38 +228,38 @@ export class KubernetesBuilder { clusterSupplier: KubernetesClustersSupplier_2, ): KubernetesProxy; // (undocumented) - protected getServiceLocator(): KubernetesServiceLocator; + protected getServiceLocator(): KubernetesServiceLocator_2; // (undocumented) protected getServiceLocatorMethod(): ServiceLocatorMethod; // (undocumented) setAuthStrategyMap(authStrategyMap: { - [key: string]: AuthenticationStrategy; + [key: string]: AuthenticationStrategy_2; }): void; // (undocumented) setClusterSupplier(clusterSupplier?: KubernetesClustersSupplier_2): this; // (undocumented) setDefaultClusterRefreshInterval(refreshInterval: Duration): this; // (undocumented) - setFetcher(fetcher?: KubernetesFetcher): this; + setFetcher(fetcher?: KubernetesFetcher_2): this; // (undocumented) setObjectsProvider(objectsProvider?: KubernetesObjectsProvider_2): this; // (undocumented) setProxy(proxy?: KubernetesProxy): this; // (undocumented) - setServiceLocator(serviceLocator?: KubernetesServiceLocator): this; + setServiceLocator(serviceLocator?: KubernetesServiceLocator_2): this; } // @public export type KubernetesBuilderReturn = Promise<{ router: express.Router; clusterSupplier: KubernetesClustersSupplier_2; - customResources: CustomResource[]; - fetcher: KubernetesFetcher; + customResources: CustomResource_2[]; + fetcher: KubernetesFetcher_2; proxy: KubernetesProxy; objectsProvider: KubernetesObjectsProvider_2; - serviceLocator: KubernetesServiceLocator; + serviceLocator: KubernetesServiceLocator_2; authStrategyMap: { - [key: string]: AuthenticationStrategy; + [key: string]: AuthenticationStrategy_2; }; }>; @@ -321,14 +341,16 @@ export type ObjectsByEntityRequest = KubernetesRequestBody; export type ObjectToFetch = k8sAuthTypes.ObjectToFetch; // @public (undocumented) -export class OidcStrategy implements AuthenticationStrategy { +export class OidcStrategy implements AuthenticationStrategy_2 { // (undocumented) getCredential( - clusterDetails: ClusterDetails, + clusterDetails: ClusterDetails_2, authConfig: KubernetesRequestAuth, - ): Promise; + ): Promise; // (undocumented) - validateCluster(authMetadata: AuthMetadata): Error[]; + presentAuthMetadata(_authMetadata: AuthMetadata_2): AuthMetadata_2; + // (undocumented) + validateCluster(authMetadata: AuthMetadata_2): Error[]; } // @public (undocumented) @@ -348,9 +370,13 @@ export interface RouterOptions { } // @public (undocumented) -export class ServiceAccountStrategy implements AuthenticationStrategy { +export class ServiceAccountStrategy implements AuthenticationStrategy_2 { // (undocumented) - getCredential(clusterDetails: ClusterDetails): Promise; + getCredential( + clusterDetails: ClusterDetails_2, + ): Promise; + // (undocumented) + presentAuthMetadata(_authMetadata: AuthMetadata_2): AuthMetadata_2; // (undocumented) validateCluster(): Error[]; } diff --git a/plugins/kubernetes-backend/src/auth/AksStrategy.ts b/plugins/kubernetes-backend/src/auth/AksStrategy.ts index 07bdf2efe0..40151314a8 100644 --- a/plugins/kubernetes-backend/src/auth/AksStrategy.ts +++ b/plugins/kubernetes-backend/src/auth/AksStrategy.ts @@ -13,8 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { ClusterDetails } from '../types/types'; -import { AuthenticationStrategy, KubernetesCredential } from './types'; +import { + AuthMetadata, + AuthenticationStrategy, + ClusterDetails, + KubernetesCredential, +} from '@backstage/plugin-kubernetes-node'; import { KubernetesRequestAuth } from '@backstage/plugin-kubernetes-common'; /** @@ -31,7 +35,12 @@ export class AksStrategy implements AuthenticationStrategy { ? { type: 'bearer token', token: token as string } : { type: 'anonymous' }; } + public validateCluster(): Error[] { return []; } + + public presentAuthMetadata(_authMetadata: AuthMetadata): AuthMetadata { + return {}; + } } diff --git a/plugins/kubernetes-backend/src/auth/AnonymousStrategy.ts b/plugins/kubernetes-backend/src/auth/AnonymousStrategy.ts index 30a9593773..0f1aad398a 100644 --- a/plugins/kubernetes-backend/src/auth/AnonymousStrategy.ts +++ b/plugins/kubernetes-backend/src/auth/AnonymousStrategy.ts @@ -14,7 +14,11 @@ * limitations under the License. */ -import { AuthenticationStrategy, KubernetesCredential } from './types'; +import { + AuthMetadata, + AuthenticationStrategy, + KubernetesCredential, +} from '@backstage/plugin-kubernetes-node'; /** * @@ -28,4 +32,8 @@ export class AnonymousStrategy implements AuthenticationStrategy { public validateCluster(): Error[] { return []; } + + public presentAuthMetadata(_authMetadata: AuthMetadata): AuthMetadata { + return {}; + } } diff --git a/plugins/kubernetes-backend/src/auth/AwsIamStrategy.ts b/plugins/kubernetes-backend/src/auth/AwsIamStrategy.ts index 53b880d60c..942b93615f 100644 --- a/plugins/kubernetes-backend/src/auth/AwsIamStrategy.ts +++ b/plugins/kubernetes-backend/src/auth/AwsIamStrategy.ts @@ -25,8 +25,12 @@ import { ANNOTATION_KUBERNETES_AWS_ASSUME_ROLE, ANNOTATION_KUBERNETES_AWS_EXTERNAL_ID, } from '@backstage/plugin-kubernetes-common'; -import { ClusterDetails } from '../types/types'; -import { AuthenticationStrategy, KubernetesCredential } from './types'; +import { + AuthMetadata, + AuthenticationStrategy, + ClusterDetails, + KubernetesCredential, +} from '@backstage/plugin-kubernetes-node'; /** * @@ -128,4 +132,8 @@ export class AwsIamStrategy implements AuthenticationStrategy { return `k8s-aws-v1.${Buffer.from(url).toString('base64url')}`; } + + public presentAuthMetadata(_authMetadata: AuthMetadata): AuthMetadata { + return {}; + } } diff --git a/plugins/kubernetes-backend/src/auth/AzureIdentityStrategy.ts b/plugins/kubernetes-backend/src/auth/AzureIdentityStrategy.ts index d0eea3c13c..a1b29a10a1 100644 --- a/plugins/kubernetes-backend/src/auth/AzureIdentityStrategy.ts +++ b/plugins/kubernetes-backend/src/auth/AzureIdentityStrategy.ts @@ -15,12 +15,16 @@ */ import { Logger } from 'winston'; -import { AuthenticationStrategy, KubernetesCredential } from './types'; import { AccessToken, DefaultAzureCredential, TokenCredential, } from '@azure/identity'; +import { + AuthMetadata, + AuthenticationStrategy, + KubernetesCredential, +} from '@backstage/plugin-kubernetes-node'; const aksScope = '6dae42f8-4368-4678-94ff-3960e28e3630/.default'; // This scope is the same for all Azure Managed Kubernetes @@ -89,4 +93,8 @@ export class AzureIdentityStrategy implements AuthenticationStrategy { private tokenExpired(): boolean { return Date.now() >= this.accessToken.expiresOnTimestamp; } + + public presentAuthMetadata(_authMetadata: AuthMetadata): AuthMetadata { + return {}; + } } diff --git a/plugins/kubernetes-backend/src/auth/DispatchStrategy.test.ts b/plugins/kubernetes-backend/src/auth/DispatchStrategy.test.ts index 7202311d6f..773e464168 100644 --- a/plugins/kubernetes-backend/src/auth/DispatchStrategy.test.ts +++ b/plugins/kubernetes-backend/src/auth/DispatchStrategy.test.ts @@ -31,6 +31,7 @@ describe('getCredential', () => { mockStrategy = { getCredential: jest.fn(), validateCluster: jest.fn(), + presentAuthMetadata: 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 f12e49b577..14a3009a65 100644 --- a/plugins/kubernetes-backend/src/auth/DispatchStrategy.ts +++ b/plugins/kubernetes-backend/src/auth/DispatchStrategy.ts @@ -14,12 +14,16 @@ * limitations under the License. */ -import { AuthenticationStrategy, KubernetesCredential } from './types'; -import { AuthMetadata, ClusterDetails } from '../types'; import { ANNOTATION_KUBERNETES_AUTH_PROVIDER, KubernetesRequestAuth, } from '@backstage/plugin-kubernetes-common'; +import { + AuthMetadata, + AuthenticationStrategy, + ClusterDetails, + KubernetesCredential, +} from '@backstage/plugin-kubernetes-node'; /** * @@ -67,4 +71,8 @@ export class DispatchStrategy implements AuthenticationStrategy { } return strategy.validateCluster(authMetadata); } + + public presentAuthMetadata(_authMetadata: AuthMetadata): AuthMetadata { + return {}; + } } diff --git a/plugins/kubernetes-backend/src/auth/GoogleServiceAccountStrategy.ts b/plugins/kubernetes-backend/src/auth/GoogleServiceAccountStrategy.ts index 2a44a31d6c..f6757d6a64 100644 --- a/plugins/kubernetes-backend/src/auth/GoogleServiceAccountStrategy.ts +++ b/plugins/kubernetes-backend/src/auth/GoogleServiceAccountStrategy.ts @@ -13,7 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { AuthenticationStrategy, KubernetesCredential } from './types'; + +import { + AuthMetadata, + AuthenticationStrategy, + KubernetesCredential, +} from '@backstage/plugin-kubernetes-node'; import * as container from '@google-cloud/container'; /** @@ -36,4 +41,8 @@ export class GoogleServiceAccountStrategy implements AuthenticationStrategy { public validateCluster(): Error[] { return []; } + + public presentAuthMetadata(_authMetadata: AuthMetadata): AuthMetadata { + return {}; + } } diff --git a/plugins/kubernetes-backend/src/auth/GoogleStrategy.ts b/plugins/kubernetes-backend/src/auth/GoogleStrategy.ts index c1290dc1fe..c48086f7f8 100644 --- a/plugins/kubernetes-backend/src/auth/GoogleStrategy.ts +++ b/plugins/kubernetes-backend/src/auth/GoogleStrategy.ts @@ -14,9 +14,13 @@ * limitations under the License. */ -import { AuthenticationStrategy, KubernetesCredential } from './types'; -import { ClusterDetails } from '../types/types'; import { KubernetesRequestAuth } from '@backstage/plugin-kubernetes-common'; +import { + AuthMetadata, + AuthenticationStrategy, + ClusterDetails, + KubernetesCredential, +} from '@backstage/plugin-kubernetes-node'; /** * @@ -35,7 +39,12 @@ export class GoogleStrategy implements AuthenticationStrategy { } return { type: 'bearer token', token: token as string }; } + public validateCluster(): Error[] { return []; } + + public presentAuthMetadata(_authMetadata: AuthMetadata): AuthMetadata { + return {}; + } } diff --git a/plugins/kubernetes-backend/src/auth/OidcStrategy.ts b/plugins/kubernetes-backend/src/auth/OidcStrategy.ts index 445799cbb8..1039f1d40c 100644 --- a/plugins/kubernetes-backend/src/auth/OidcStrategy.ts +++ b/plugins/kubernetes-backend/src/auth/OidcStrategy.ts @@ -18,8 +18,12 @@ import { ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER, KubernetesRequestAuth, } from '@backstage/plugin-kubernetes-common'; -import { AuthenticationStrategy, KubernetesCredential } from './types'; -import { AuthMetadata, ClusterDetails } from '../types/types'; +import { + AuthMetadata, + AuthenticationStrategy, + ClusterDetails, + KubernetesCredential, +} from '@backstage/plugin-kubernetes-node'; /** * @@ -57,4 +61,8 @@ export class OidcStrategy implements AuthenticationStrategy { } return []; } + + public presentAuthMetadata(_authMetadata: AuthMetadata): AuthMetadata { + return {}; + } } diff --git a/plugins/kubernetes-backend/src/auth/ServiceAccountStrategy.ts b/plugins/kubernetes-backend/src/auth/ServiceAccountStrategy.ts index 714b2a94d1..aff6e61a64 100644 --- a/plugins/kubernetes-backend/src/auth/ServiceAccountStrategy.ts +++ b/plugins/kubernetes-backend/src/auth/ServiceAccountStrategy.ts @@ -14,10 +14,14 @@ * limitations under the License. */ +import { + AuthMetadata, + AuthenticationStrategy, + ClusterDetails, + KubernetesCredential, +} from '@backstage/plugin-kubernetes-node'; import { KubeConfig, User } from '@kubernetes/client-node'; import fs from 'fs-extra'; -import { AuthenticationStrategy, KubernetesCredential } from './types'; -import { ClusterDetails } from '../types/types'; /** * @@ -45,4 +49,8 @@ export class ServiceAccountStrategy implements AuthenticationStrategy { public validateCluster(): Error[] { return []; } + + public presentAuthMetadata(_authMetadata: AuthMetadata): AuthMetadata { + return {}; + } } diff --git a/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.test.ts b/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.test.ts index 1720ef5ed6..ad3203a16d 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.test.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.test.ts @@ -32,6 +32,7 @@ describe('ConfigClusterLocator', () => { authStrategy = { getCredential: jest.fn(), validateCluster: jest.fn().mockReturnValue([]), + presentAuthMetadata: jest.fn(), }; }); diff --git a/plugins/kubernetes-backend/src/cluster-locator/index.test.ts b/plugins/kubernetes-backend/src/cluster-locator/index.test.ts index ded0faac5b..beb9fc9cfe 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/index.test.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/index.test.ts @@ -53,6 +53,7 @@ describe('getCombinedClusterSupplier', () => { const mockStrategy: jest.Mocked = { getCredential: jest.fn(), validateCluster: jest.fn().mockReturnValue([]), + presentAuthMetadata: jest.fn(), }; const clusterSupplier = getCombinedClusterSupplier( diff --git a/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts b/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts index 42f953a2fe..5030adaa81 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts @@ -16,6 +16,9 @@ import { ANNOTATION_KUBERNETES_AUTH_PROVIDER, + ANNOTATION_KUBERNETES_AWS_ASSUME_ROLE, + ANNOTATION_KUBERNETES_AWS_EXTERNAL_ID, + ANNOTATION_KUBERNETES_DASHBOARD_URL, ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER, KubernetesRequestAuth, } from '@backstage/plugin-kubernetes-common'; @@ -50,6 +53,7 @@ import { kubernetesObjectsProviderExtensionPoint, kubernetesFetcherExtensionPoint, kubernetesServiceLocatorExtensionPoint, + AuthMetadata, } from '@backstage/plugin-kubernetes-node'; import { ExtendedHttpServer } from '@backstage/backend-app-api'; @@ -177,151 +181,75 @@ describe('API integration tests', () => { ], }); }); - }); - describe('post /services/:serviceId', () => { - it('happy path: lists kubernetes objects without auth in request body', async () => { - const response = await request(app).post( - '/api/kubernetes/services/test-service', - ); - expect(response.status).toEqual(200); - expect(response.body).toEqual(happyK8SResult); - }); - - it('happy path: lists kubernetes objects with auth in request body', async () => { - const response = await request(app) - .post('/api/kubernetes/services/test-service') - .send({ auth: { google: 'google_token_123' } }) - .set('Content-Type', 'application/json'); - expect(response.status).toEqual(200); - expect(response.body).toEqual(happyK8SResult); - }); - - it('internal error: lists kubernetes objects', async () => { - objectsProviderMock.getKubernetesObjectsByEntity.mockRejectedValue( - Error('some internal error'), - ); - - const response = await request(app).post( - '/api/kubernetes/services/test-service', - ); - - expect(response.status).toEqual(500); - expect(response.body).toEqual({ error: 'some internal error' }); - }); - - it('custom service locator', async () => { - const someCluster = { - name: 'some-cluster', - url: 'https://localhost:1234', - authMetadata: { - [ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'serviceAccount', - serviceAccountToken: 'placeholder-token', - }, - }; - const clusters: ClusterDetails[] = [ - someCluster, - { - name: 'some-other-cluster', - url: 'https://localhost:1235', - authMetadata: { [ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'google' }, - }, - ]; - - const pod = { metadata: { name: 'pod1' } }; - - const mockServiceLocator: jest.Mocked = { - getClustersByEntity: jest.fn().mockResolvedValue({ - clusters: [someCluster], - }), - }; - - const mockFetcher: jest.Mocked = { - fetchPodMetricsByNamespaces: jest + it('happy path: lists clusters with custom AuthStrategy and custom auth metadata', async () => { + objectsProviderMock = { + getKubernetesObjectsByEntity: jest .fn() - .mockResolvedValue({ errors: [], responses: [] }), - fetchObjectsForService: jest.fn().mockResolvedValue({ - errors: [], - responses: [{ type: 'pods', resources: [pod] }], - }), + .mockResolvedValue(happyK8SResult), + getCustomResourcesByEntity: jest.fn().mockResolvedValue(happyK8SResult), }; - const { server } = await startTestBackend({ - features: [ - minimalValidConfigService, - import('@backstage/plugin-kubernetes-backend/alpha'), - withClusters(clusters), - createBackendModule({ - pluginId: 'kubernetes', - moduleId: 'testFetcher', - register(env) { - env.registerInit({ - deps: { extension: kubernetesFetcherExtensionPoint }, - async init({ extension }) { - extension.addFetcher(mockFetcher); - }, - }); - }, - }), - createBackendModule({ - pluginId: 'kubernetes', - moduleId: 'testServiceLocator', - register(env) { - env.registerInit({ - deps: { extension: kubernetesServiceLocatorExtensionPoint }, - async init({ extension }) { - extension.addServiceLocator(mockServiceLocator); - }, - }); - }, - }), - ], - }); - app = server; - - const response = await request(app) - .post('/api/kubernetes/services/test-service') - .send({ entity: { metadata: { name: 'thing' } } }); - - expect(response.body).toEqual({ - items: [ - { - cluster: { name: someCluster.name }, - errors: [], - podMetrics: [], - resources: [{ type: 'pods', resources: [pod] }], - }, - ], - }); - expect(response.status).toEqual(200); - }); - - it('reads auth data for custom strategy', async () => { - const mockFetcher = { - fetchPodMetricsByNamespaces: jest - .fn() - .mockResolvedValue({ errors: [], responses: [] }), - fetchObjectsForService: jest.fn().mockResolvedValue({ - errors: [], - responses: [ - { type: 'pods', resources: [{ metadata: { name: 'pod1' } }] }, - ], - }), - }; - - const { server } = await startTestBackend({ - features: [ - minimalValidConfigService, - import('@backstage/plugin-kubernetes-backend/alpha'), - withClusters([ + const clusterSupplierMock = { + getClusters: jest.fn().mockResolvedValue( + Promise.resolve([ { - name: 'custom-cluster', - url: 'http://my.cluster.url', + name: 'some-cluster', + url: 'https://localhost:1234', authMetadata: { - [ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'custom', + [ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'customAuth', + [ANNOTATION_KUBERNETES_DASHBOARD_URL]: + 'https://127.0.0.1:8443/dashboard', + [ANNOTATION_KUBERNETES_AWS_EXTERNAL_ID]: '12650152165654', + [ANNOTATION_KUBERNETES_AWS_ASSUME_ROLE]: 'my_aws_role', }, }, ]), + ), + }; + + const { server } = await startTestBackend({ + features: [ + mockServices.rootConfig.factory({ + data: { + kubernetes: { + serviceLocatorMethod: { + type: 'multiTenant', + }, + clusterLocatorMethods: [ + { + type: 'config', + clusters: [], + }, + ], + }, + }, + }), + import('@backstage/plugin-kubernetes-backend/alpha'), + createBackendModule({ + pluginId: 'kubernetes', + moduleId: 'testObjectsProvider', + register(env) { + env.registerInit({ + deps: { extension: kubernetesObjectsProviderExtensionPoint }, + async init({ extension }) { + extension.addObjectsProvider(objectsProviderMock); + }, + }); + }, + }), + createBackendModule({ + pluginId: 'kubernetes', + moduleId: 'testClusterSupplier', + register(env) { + env.registerInit({ + deps: { extension: kubernetesClusterSupplierExtensionPoint }, + async init({ extension }) { + extension.addClusterSupplier(clusterSupplierMock); + }, + }); + }, + }), createBackendModule({ pluginId: 'kubernetes', moduleId: 'testAuthStrategy', @@ -329,7 +257,7 @@ describe('API integration tests', () => { env.registerInit({ deps: { extension: kubernetesAuthStrategyExtensionPoint }, async init({ extension }) { - extension.addAuthStrategy('custom', { + extension.addAuthStrategy('customAuth', { getCredential: jest .fn< Promise, @@ -340,322 +268,538 @@ describe('API integration tests', () => { token: requestAuth.custom as string, })), validateCluster: jest.fn().mockReturnValue([]), + presentAuthMetadata: ( + authMetadata: AuthMetadata, + ): AuthMetadata => { + const authMetadataFilter = Object.entries(authMetadata) + .filter(([key, _value]) => { + return [ + ANNOTATION_KUBERNETES_DASHBOARD_URL, + ANNOTATION_KUBERNETES_AWS_ASSUME_ROLE, + ].includes(key); + }) + .reduce( + ( + accumulator: AuthMetadata, + currentValue: [string, string], + ) => { + accumulator[currentValue[0]] = currentValue[1]; + return accumulator; + }, + {}, + ); + + return authMetadataFilter; + }, }); }, }); }, }), - createBackendModule({ - pluginId: 'kubernetes', - moduleId: 'testFetcher', - register(env) { - env.registerInit({ - deps: { extension: kubernetesFetcherExtensionPoint }, - async init({ extension }) { - extension.addFetcher(mockFetcher); - }, - }); - }, - }), ], }); + }); - app = server; + describe('post /services/:serviceId', () => { + it('happy path: lists kubernetes objects without auth in request body', async () => { + const response = await request(app).post( + '/api/kubernetes/services/test-service', + ); + expect(response.status).toEqual(200); + expect(response.body).toEqual(happyK8SResult); + }); - await request(app) - .post('/api/kubernetes/services/test-service') - .send({ - entity: { metadata: { name: 'thing' } }, - auth: { custom: 'custom-token' }, + it('happy path: lists kubernetes objects with auth in request body', async () => { + const response = await request(app) + .post('/api/kubernetes/services/test-service') + .send({ auth: { google: 'google_token_123' } }) + .set('Content-Type', 'application/json'); + expect(response.status).toEqual(200); + expect(response.body).toEqual(happyK8SResult); + }); + + it('internal error: lists kubernetes objects', async () => { + objectsProviderMock.getKubernetesObjectsByEntity.mockRejectedValue( + Error('some internal error'), + ); + + const response = await request(app).post( + '/api/kubernetes/services/test-service', + ); + + expect(response.status).toEqual(500); + expect(response.body).toEqual({ error: 'some internal error' }); + }); + + it('custom service locator', async () => { + const someCluster = { + name: 'some-cluster', + url: 'https://localhost:1234', + authMetadata: { + [ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'serviceAccount', + serviceAccountToken: 'placeholder-token', + }, + }; + const clusters: ClusterDetails[] = [ + someCluster, + { + name: 'some-other-cluster', + url: 'https://localhost:1235', + authMetadata: { [ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'google' }, + }, + ]; + + const pod = { metadata: { name: 'pod1' } }; + + const mockServiceLocator: jest.Mocked = { + getClustersByEntity: jest.fn().mockResolvedValue({ + clusters: [someCluster], + }), + }; + + const mockFetcher: jest.Mocked = { + fetchPodMetricsByNamespaces: jest + .fn() + .mockResolvedValue({ errors: [], responses: [] }), + fetchObjectsForService: jest.fn().mockResolvedValue({ + errors: [], + responses: [{ type: 'pods', resources: [pod] }], + }), + }; + + const { server } = await startTestBackend({ + features: [ + minimalValidConfigService, + import('@backstage/plugin-kubernetes-backend/alpha'), + withClusters(clusters), + createBackendModule({ + pluginId: 'kubernetes', + moduleId: 'testFetcher', + register(env) { + env.registerInit({ + deps: { extension: kubernetesFetcherExtensionPoint }, + async init({ extension }) { + extension.addFetcher(mockFetcher); + }, + }); + }, + }), + createBackendModule({ + pluginId: 'kubernetes', + moduleId: 'testServiceLocator', + register(env) { + env.registerInit({ + deps: { extension: kubernetesServiceLocatorExtensionPoint }, + async init({ extension }) { + extension.addServiceLocator(mockServiceLocator); + }, + }); + }, + }), + ], + }); + app = server; + + const response = await request(app) + .post('/api/kubernetes/services/test-service') + .send({ entity: { metadata: { name: 'thing' } } }); + + expect(response.body).toEqual({ + items: [ + { + cluster: { name: someCluster.name }, + errors: [], + podMetrics: [], + resources: [{ type: 'pods', resources: [pod] }], + }, + ], + }); + expect(response.status).toEqual(200); + }); + + it('reads auth data for custom strategy', async () => { + const mockFetcher = { + fetchPodMetricsByNamespaces: jest + .fn() + .mockResolvedValue({ errors: [], responses: [] }), + fetchObjectsForService: jest.fn().mockResolvedValue({ + errors: [], + responses: [ + { type: 'pods', resources: [{ metadata: { name: 'pod1' } }] }, + ], + }), + }; + + const { server } = await startTestBackend({ + features: [ + minimalValidConfigService, + import('@backstage/plugin-kubernetes-backend/alpha'), + withClusters([ + { + name: 'custom-cluster', + url: 'http://my.cluster.url', + authMetadata: { + [ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'custom', + }, + }, + ]), + createBackendModule({ + pluginId: 'kubernetes', + moduleId: 'testAuthStrategy', + register(env) { + env.registerInit({ + deps: { extension: kubernetesAuthStrategyExtensionPoint }, + async init({ extension }) { + extension.addAuthStrategy('custom', { + getCredential: jest + .fn< + Promise, + [ClusterDetails, KubernetesRequestAuth] + >() + .mockImplementation(async (_, requestAuth) => ({ + type: 'bearer token', + token: requestAuth.custom as string, + })), + validateCluster: jest.fn().mockReturnValue([]), + presentAuthMetadata: jest.fn().mockReturnValue({}), + }); + }, + }); + }, + }), + createBackendModule({ + pluginId: 'kubernetes', + moduleId: 'testFetcher', + register(env) { + env.registerInit({ + deps: { extension: kubernetesFetcherExtensionPoint }, + async init({ extension }) { + extension.addFetcher(mockFetcher); + }, + }); + }, + }), + ], }); - expect(mockFetcher.fetchObjectsForService).toHaveBeenCalledWith( - expect.objectContaining({ - credential: { type: 'bearer token', token: 'custom-token' }, - }), - ); - }); - }); + app = server; - describe('/proxy', () => { - const worker = setupServer(); - setupRequestMockHandlers(worker); + await request(app) + .post('/api/kubernetes/services/test-service') + .send({ + entity: { metadata: { name: 'thing' } }, + auth: { custom: 'custom-token' }, + }); - beforeEach(() => { - worker.use( - rest.post( - 'https://localhost:1234/api/v1/namespaces', - (req, res, ctx) => { - if (!req.headers.get('Authorization')) { - return res(ctx.status(401)); - } - return req - .arrayBuffer() - .then(body => - res( - ctx.set('content-type', `${req.headers.get('content-type')}`), - ctx.body(body), - ), - ); - }, - ), - ); + expect(mockFetcher.fetchObjectsForService).toHaveBeenCalledWith( + expect.objectContaining({ + credential: { type: 'bearer token', token: 'custom-token' }, + }), + ); + }); }); - it('forwards request body to k8s', async () => { - const namespaceManifest = { - kind: 'Namespace', - apiVersion: 'v1', - metadata: { name: 'new-ns' }, - }; + describe('/proxy', () => { + const worker = setupServer(); + setupRequestMockHandlers(worker); - const proxyEndpointRequest = request(app) - .post('/api/kubernetes/proxy/api/v1/namespaces') - .set(HEADER_KUBERNETES_CLUSTER, 'some-cluster') - .set(HEADER_KUBERNETES_AUTH, 'randomtoken') - .send(namespaceManifest); - worker.use(rest.all(proxyEndpointRequest.url, req => req.passthrough())); - const response = await proxyEndpointRequest; + beforeEach(() => { + worker.use( + rest.post( + 'https://localhost:1234/api/v1/namespaces', + (req, res, ctx) => { + if (!req.headers.get('Authorization')) { + return res(ctx.status(401)); + } + return req + .arrayBuffer() + .then(body => + res( + ctx.set( + 'content-type', + `${req.headers.get('content-type')}`, + ), + ctx.body(body), + ), + ); + }, + ), + ); + }); - expect(response.body).toStrictEqual(namespaceManifest); - }); + it('forwards request body to k8s', async () => { + const namespaceManifest = { + kind: 'Namespace', + apiVersion: 'v1', + metadata: { name: 'new-ns' }, + }; - it('supports yaml content type', async () => { - const yamlManifest = `--- + const proxyEndpointRequest = request(app) + .post('/api/kubernetes/proxy/api/v1/namespaces') + .set(HEADER_KUBERNETES_CLUSTER, 'some-cluster') + .set(HEADER_KUBERNETES_AUTH, 'randomtoken') + .send(namespaceManifest); + worker.use( + rest.all(proxyEndpointRequest.url, req => req.passthrough()), + ); + const response = await proxyEndpointRequest; + + expect(response.body).toStrictEqual(namespaceManifest); + }); + + it('supports yaml content type', async () => { + const yamlManifest = `--- kind: Namespace apiVersion: v1 metadata: name: new-ns `; - const proxyEndpointRequest = request(app) - .post('/api/kubernetes/proxy/api/v1/namespaces') - .set(HEADER_KUBERNETES_CLUSTER, 'some-cluster') - .set(HEADER_KUBERNETES_AUTH, 'randomtoken') - .set('content-type', 'application/yaml') - .send(yamlManifest); + const proxyEndpointRequest = request(app) + .post('/api/kubernetes/proxy/api/v1/namespaces') + .set(HEADER_KUBERNETES_CLUSTER, 'some-cluster') + .set(HEADER_KUBERNETES_AUTH, 'randomtoken') + .set('content-type', 'application/yaml') + .send(yamlManifest); - worker.use(rest.all(proxyEndpointRequest.url, req => req.passthrough())); + worker.use( + rest.all(proxyEndpointRequest.url, req => req.passthrough()), + ); - const response = await proxyEndpointRequest; - expect(response.text).toEqual(yamlManifest); - }); - - it('returns 403 response when permission blocks endpoint', async () => { - permissionsMock.authorize.mockResolvedValue([ - { result: AuthorizeResult.DENY }, - ]); - - const { server } = await startTestBackend({ - features: [ - minimalValidConfigService, - permissionsMock.factory, - import('@backstage/plugin-kubernetes-backend/alpha'), - ], + const response = await proxyEndpointRequest; + expect(response.text).toEqual(yamlManifest); }); - app = server; - const proxyEndpointRequest = request(app) - .post('/api/kubernetes/proxy/api/v1/namespaces') - .set(HEADER_KUBERNETES_CLUSTER, 'some-cluster') - .set(HEADER_KUBERNETES_AUTH, 'randomtoken') - .send({ - kind: 'Namespace', - apiVersion: 'v1', - metadata: { name: 'new-ns' }, + it('returns 403 response when permission blocks endpoint', async () => { + permissionsMock.authorize.mockResolvedValue([ + { result: AuthorizeResult.DENY }, + ]); + + const { server } = await startTestBackend({ + features: [ + minimalValidConfigService, + permissionsMock.factory, + import('@backstage/plugin-kubernetes-backend/alpha'), + ], + }); + app = server; + + const proxyEndpointRequest = request(app) + .post('/api/kubernetes/proxy/api/v1/namespaces') + .set(HEADER_KUBERNETES_CLUSTER, 'some-cluster') + .set(HEADER_KUBERNETES_AUTH, 'randomtoken') + .send({ + kind: 'Namespace', + apiVersion: 'v1', + metadata: { name: 'new-ns' }, + }); + + worker.use( + rest.all(proxyEndpointRequest.url, req => req.passthrough()), + ); + + const response = await proxyEndpointRequest; + + 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: [] })); + }, + ), + ); + + const { server } = await startTestBackend({ + features: [ + minimalValidConfigService, + import('@backstage/plugin-kubernetes-backend/alpha'), + withClusters([ + { + name: 'custom-cluster', + url: 'http://my.cluster.url', + authMetadata: { + [ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'custom', + }, + }, + ]), + createBackendModule({ + pluginId: 'kubernetes', + moduleId: 'testAuthStrategy', + register(env) { + env.registerInit({ + deps: { extension: kubernetesAuthStrategyExtensionPoint }, + async init({ extension }) { + extension.addAuthStrategy('custom', { + getCredential: jest + .fn< + Promise, + [ClusterDetails, KubernetesRequestAuth] + >() + .mockResolvedValue({ type: 'anonymous' }), + validateCluster: jest.fn().mockReturnValue([]), + presentAuthMetadata: jest.fn().mockReturnValue({}), + }); + }, + }); + }, + }), + ], }); - worker.use(rest.all(proxyEndpointRequest.url, req => req.passthrough())); + const proxyEndpointRequest = request(server) + .get('/api/kubernetes/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; - const response = await proxyEndpointRequest; - - 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: [] })); - }), - ); - - const { server } = await startTestBackend({ - features: [ - minimalValidConfigService, - import('@backstage/plugin-kubernetes-backend/alpha'), - withClusters([ - { - name: 'custom-cluster', - url: 'http://my.cluster.url', - authMetadata: { [ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'custom' }, - }, - ]), - createBackendModule({ - pluginId: 'kubernetes', - moduleId: 'testAuthStrategy', - register(env) { - env.registerInit({ - deps: { extension: kubernetesAuthStrategyExtensionPoint }, - async init({ extension }) { - extension.addAuthStrategy('custom', { - getCredential: jest - .fn< - Promise, - [ClusterDetails, KubernetesRequestAuth] - >() - .mockResolvedValue({ type: 'anonymous' }), - validateCluster: jest.fn().mockReturnValue([]), - }); - }, - }); - }, - }), - ], + expect(response.body).toStrictEqual({ items: [] }); }); - const proxyEndpointRequest = request(server) - .get('/api/kubernetes/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: [] }); - }); - - it('reads custom auth metadata from config', async () => { - const authStrategy = { - getCredential: jest.fn().mockResolvedValue({ type: 'anonymous' }), - validateCluster: jest.fn().mockReturnValue([]), - }; - worker.use( - rest.get('http://my.cluster/api', (_req, res, ctx) => - res(ctx.json({})), - ), - ); - const { server } = await startTestBackend({ - features: [ - mockServices.rootConfig.factory({ - data: { - kubernetes: { - serviceLocatorMethod: { type: 'multiTenant' }, - clusterLocatorMethods: [ - { - type: 'config', - clusters: [ - { - name: 'cluster', - url: 'http://my.cluster', - authProvider: 'custom', - authMetadata: { 'custom-key': 'custom-value' }, - }, - ], + it('reads custom auth metadata from config', async () => { + const authStrategy = { + getCredential: jest.fn().mockResolvedValue({ type: 'anonymous' }), + validateCluster: jest.fn().mockReturnValue([]), + presentAuthMetadata: jest.fn().mockReturnValue({}), + }; + worker.use( + rest.get('http://my.cluster/api', (_req, res, ctx) => + res(ctx.json({})), + ), + ); + const { server } = await startTestBackend({ + features: [ + mockServices.rootConfig.factory({ + data: { + kubernetes: { + serviceLocatorMethod: { type: 'multiTenant' }, + clusterLocatorMethods: [ + { + type: 'config', + clusters: [ + { + name: 'cluster', + url: 'http://my.cluster', + authProvider: 'custom', + authMetadata: { 'custom-key': 'custom-value' }, + }, + ], + }, + ], + }, + }, + }), + import('@backstage/plugin-kubernetes-backend/alpha'), + createBackendModule({ + pluginId: 'kubernetes', + moduleId: 'testAuthStrategy', + register(env) { + env.registerInit({ + deps: { extension: kubernetesAuthStrategyExtensionPoint }, + async init({ extension }) { + extension.addAuthStrategy('custom', authStrategy); }, - ], + }); }, - }, + }), + ], + }); + app = server; + + const proxyEndpointRequest = request(app).get( + '/api/kubernetes/proxy/api', + ); + worker.use( + rest.all(proxyEndpointRequest.url, req => req.passthrough()), + ); + const response = await proxyEndpointRequest; + + expect(response.body).toStrictEqual({}); + expect(authStrategy.getCredential).toHaveBeenCalledWith( + expect.objectContaining({ + authMetadata: expect.objectContaining({ + 'custom-key': 'custom-value', + }), }), - import('@backstage/plugin-kubernetes-backend/alpha'), - createBackendModule({ - pluginId: 'kubernetes', - moduleId: 'testAuthStrategy', - register(env) { - env.registerInit({ - deps: { extension: kubernetesAuthStrategyExtensionPoint }, - async init({ extension }) { - extension.addAuthStrategy('custom', authStrategy); - }, - }); - }, - }), - ], + expect.anything(), + ); }); - app = server; + }); - const proxyEndpointRequest = request(app).get( - '/api/kubernetes/proxy/api', + it('forbids custom auth strategies with dashes', () => { + const throwError = () => + startTestBackend({ + features: [ + import('@backstage/plugin-kubernetes-backend/alpha'), + createBackendModule({ + pluginId: 'kubernetes', + moduleId: 'testAuthStrategy', + register(env) { + env.registerInit({ + deps: { extension: kubernetesAuthStrategyExtensionPoint }, + async init({ extension }) { + extension.addAuthStrategy('custom-strategy', { + getCredential: jest + .fn< + Promise, + [ClusterDetails, KubernetesRequestAuth] + >() + .mockResolvedValue({ type: 'anonymous' }), + validateCluster: jest.fn().mockReturnValue([]), + presentAuthMetadata: jest.fn().mockReturnValue({}), + }); + }, + }); + }, + }), + ], + }); + return expect(throwError).rejects.toThrow( + 'Strategy name can not include dashes', ); - worker.use(rest.all(proxyEndpointRequest.url, req => req.passthrough())); - const response = await proxyEndpointRequest; + }); - expect(response.body).toStrictEqual({}); - expect(authStrategy.getCredential).toHaveBeenCalledWith( - expect.objectContaining({ - authMetadata: expect.objectContaining({ - 'custom-key': 'custom-value', - }), + it('serves permission integration endpoint', async () => { + const response = await request(app).get( + '/api/kubernetes/.well-known/backstage/permissions/metadata', + ); + + expect(response.status).toEqual(200); + expect(response.body).toMatchObject({ + permissions: [ + { type: 'basic', name: 'kubernetes.proxy', attributes: {} }, + ], + rules: [], + }); + }); + + it('fails when an unsupported serviceLocator type is specified', () => { + return expect(() => + startTestBackend({ + features: [ + mockServices.rootConfig.factory({ + data: { + kubernetes: { + serviceLocatorMethod: { type: 'unsupported' }, + clusterLocatorMethods: [{ type: 'config', clusters: [] }], + }, + }, + }), + import('@backstage/plugin-kubernetes-backend/alpha'), + ], }), - expect.anything(), + ).rejects.toThrow( + 'Unsupported kubernetes.serviceLocatorMethod "unsupported"', ); }); }); - - it('forbids custom auth strategies with dashes', () => { - const throwError = () => - startTestBackend({ - features: [ - import('@backstage/plugin-kubernetes-backend/alpha'), - createBackendModule({ - pluginId: 'kubernetes', - moduleId: 'testAuthStrategy', - register(env) { - env.registerInit({ - deps: { extension: kubernetesAuthStrategyExtensionPoint }, - async init({ extension }) { - extension.addAuthStrategy('custom-strategy', { - getCredential: jest - .fn< - Promise, - [ClusterDetails, KubernetesRequestAuth] - >() - .mockResolvedValue({ type: 'anonymous' }), - validateCluster: jest.fn().mockReturnValue([]), - }); - }, - }); - }, - }), - ], - }); - return expect(throwError).rejects.toThrow( - 'Strategy name can not include dashes', - ); - }); - - it('serves permission integration endpoint', async () => { - const response = await request(app).get( - '/api/kubernetes/.well-known/backstage/permissions/metadata', - ); - - expect(response.status).toEqual(200); - expect(response.body).toMatchObject({ - permissions: [ - { type: 'basic', name: 'kubernetes.proxy', attributes: {} }, - ], - rules: [], - }); - }); - - it('fails when an unsupported serviceLocator type is specified', () => { - return expect(() => - startTestBackend({ - features: [ - mockServices.rootConfig.factory({ - data: { - kubernetes: { - serviceLocatorMethod: { type: 'unsupported' }, - clusterLocatorMethods: [{ type: 'config', clusters: [] }], - }, - }, - }), - import('@backstage/plugin-kubernetes-backend/alpha'), - ], - }), - ).rejects.toThrow( - 'Unsupported kubernetes.serviceLocatorMethod "unsupported"', - ); - }); }); diff --git a/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts b/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts index a4a577db85..81cc3d471d 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts @@ -29,7 +29,6 @@ import { Logger } from 'winston'; import { getCombinedClusterSupplier } from '../cluster-locator'; import { - AuthenticationStrategy, AnonymousStrategy, DispatchStrategy, GoogleStrategy, @@ -45,17 +44,19 @@ import { addResourceRoutesToRouter } from '../routes/resourcesRoutes'; import { MultiTenantServiceLocator } from '../service-locator/MultiTenantServiceLocator'; import { SingleTenantServiceLocator } from '../service-locator/SingleTenantServiceLocator'; import { - CustomResource, - KubernetesFetcher, KubernetesObjectsProviderOptions, - KubernetesObjectTypes, - KubernetesServiceLocator, ObjectsByEntityRequest, ServiceLocatorMethod, } from '../types/types'; import { + AuthenticationStrategy, + AuthMetadata, + CustomResource, KubernetesClustersSupplier, + KubernetesFetcher, KubernetesObjectsProvider, + KubernetesObjectTypes, + KubernetesServiceLocator, } from '@backstage/plugin-kubernetes-node'; import { DEFAULT_OBJECTS, @@ -369,11 +370,20 @@ export class KubernetesBuilder { items: clusterDetails.map(cd => { const oidcTokenProvider = cd.authMetadata[ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER]; + const authProvider = + cd.authMetadata[ANNOTATION_KUBERNETES_AUTH_PROVIDER]; + const strategy = this.getAuthStrategyMap()[authProvider]; + let auth: AuthMetadata = {}; + if (strategy) { + auth = strategy.presentAuthMetadata(cd.authMetadata); + } + return { name: cd.name, dashboardUrl: cd.dashboardUrl, - authProvider: cd.authMetadata[ANNOTATION_KUBERNETES_AUTH_PROVIDER], + authProvider, ...(oidcTokenProvider && { oidcTokenProvider }), + ...(auth && Object.keys(auth).length !== 0 && { auth }), }; }), }); diff --git a/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.test.ts b/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.test.ts index 1faf2f0673..a50d0b37a2 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.test.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.test.ts @@ -196,6 +196,7 @@ describe('KubernetesFanOutHandler', () => { >() .mockResolvedValue({ type: 'anonymous' }), validateCluster: jest.fn().mockReturnValue([]), + presentAuthMetadata: jest.fn(), }, config, }); @@ -1147,6 +1148,7 @@ describe('KubernetesFanOutHandler', () => { >() .mockResolvedValue({ type: 'bearer token', token: 'token' }), validateCluster: jest.fn().mockReturnValue([]), + presentAuthMetadata: jest.fn(), }, config, }); diff --git a/plugins/kubernetes-backend/src/service/KubernetesProxy.test.ts b/plugins/kubernetes-backend/src/service/KubernetesProxy.test.ts index 9bee21867d..a9f49be780 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesProxy.test.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesProxy.test.ts @@ -131,6 +131,7 @@ describe('KubernetesProxy', () => { >() .mockResolvedValue({ type: 'anonymous' }), validateCluster: jest.fn(), + presentAuthMetadata: jest.fn(), }; proxy = new KubernetesProxy({ logger, clusterSupplier, authStrategy }); permissionApi.authorize.mockResolvedValue([ @@ -514,6 +515,7 @@ describe('KubernetesProxy', () => { .fn() .mockReturnValue({ type: 'bearer token', token: 'MY_TOKEN3' }), validateCluster: jest.fn(), + presentAuthMetadata: jest.fn(), }; proxy = new KubernetesProxy({ diff --git a/plugins/kubernetes-node/api-report.md b/plugins/kubernetes-node/api-report.md index 2272035742..aba1cdd1ae 100644 --- a/plugins/kubernetes-node/api-report.md +++ b/plugins/kubernetes-node/api-report.md @@ -25,6 +25,8 @@ export interface AuthenticationStrategy { authConfig: KubernetesRequestAuth, ): Promise; // (undocumented) + presentAuthMetadata(authMetadata: AuthMetadata): AuthMetadata; + // (undocumented) validateCluster(authMetadata: AuthMetadata): Error[]; } diff --git a/plugins/kubernetes-node/src/types/types.ts b/plugins/kubernetes-node/src/types/types.ts index a0f48d0c26..34a3286eb7 100644 --- a/plugins/kubernetes-node/src/types/types.ts +++ b/plugins/kubernetes-node/src/types/types.ts @@ -151,6 +151,7 @@ export interface AuthenticationStrategy { authConfig: KubernetesRequestAuth, ): Promise; validateCluster(authMetadata: AuthMetadata): Error[]; + presentAuthMetadata(authMetadata: AuthMetadata): AuthMetadata; } /**