From 6c5d7f6f4041fd2805de6de9d06db272fd740aef Mon Sep 17 00:00:00 2001 From: Andres Mauricio Gomez P Date: Mon, 27 Nov 2023 09:53:10 -0500 Subject: [PATCH] Refactoring KubernetesBuilder.test.ts to use kubernetesObjectsProviderExtensionPoint, kubernetesClusterSupplierExtensionPoint, kubernetesAuthStrategyExtensionPoint, kubernetesFetcherExtensionPoint and kubernetesServiceLocatorExtensionPoint Signed-off-by: Andres Mauricio Gomez P --- .changeset/plenty-falcons-travel.md | 8 +- plugins/kubernetes-backend/api-report.md | 160 +---- plugins/kubernetes-backend/src/auth/types.ts | 2 + .../src/service/KubernetesBuilder.test.ts | 666 ++++++++++++------ plugins/kubernetes-backend/src/types/types.ts | 23 +- plugins/kubernetes-node/api-report.md | 187 +++++ plugins/kubernetes-node/package.json | 1 - plugins/kubernetes-node/src/types/types.ts | 23 +- yarn.lock | 1 - 9 files changed, 678 insertions(+), 393 deletions(-) diff --git a/.changeset/plenty-falcons-travel.md b/.changeset/plenty-falcons-travel.md index cfc3ffd6f5..0c8706cde0 100644 --- a/.changeset/plenty-falcons-travel.md +++ b/.changeset/plenty-falcons-travel.md @@ -3,4 +3,10 @@ '@backstage/plugin-kubernetes-backend': patch --- -The `kubernetes-node` plugin has been modified to house a new extension points for Kubernetes backend plugin; `KubernetesClusterSupplierExtensionPoint` is introduced . The `kubernetes-backend` plugin was modified to use this new extension point. +The `kubernetes-node` plugin has been modified to house a new extension points for Kubernetes backend plugin; +`KubernetesClusterSupplierExtensionPoint` is introduced . +`kubernetesAuthStrategyExtensionPoint` is introduced . +`kubernetesFetcherExtensionPoint` is introduced . +`kubernetesServiceLocatorExtensionPoint` is introduced . + +The `kubernetes-backend` plugin was modified to use this new extension point. diff --git a/plugins/kubernetes-backend/api-report.md b/plugins/kubernetes-backend/api-report.md index f79b14ede0..04c7cf8b1e 100644 --- a/plugins/kubernetes-backend/api-report.md +++ b/plugins/kubernetes-backend/api-report.md @@ -3,21 +3,25 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { AuthenticationStrategy } from '@backstage/plugin-kubernetes-node'; +import { AuthMetadata } from '@backstage/plugin-kubernetes-node'; import { CatalogApi } from '@backstage/catalog-client'; +import { ClusterDetails } from '@backstage/plugin-kubernetes-node'; import { Config } from '@backstage/config'; -import type { CustomResourceMatcher } from '@backstage/plugin-kubernetes-common'; +import { CustomResource } from '@backstage/plugin-kubernetes-node'; import { CustomResourcesByEntity } from '@backstage/plugin-kubernetes-node'; import { Duration } from 'luxon'; -import { Entity } from '@backstage/catalog-model'; import express from 'express'; -import type { FetchResponse } from '@backstage/plugin-kubernetes-common'; -import type { JsonObject } from '@backstage/types'; -import type { KubernetesFetchError } from '@backstage/plugin-kubernetes-common'; +import { KubernetesClustersSupplier } from '@backstage/plugin-kubernetes-node'; +import { KubernetesCredential } from '@backstage/plugin-kubernetes-node'; +import { KubernetesFetcher } from '@backstage/plugin-kubernetes-node'; import { KubernetesObjectsByEntity } from '@backstage/plugin-kubernetes-node'; import { KubernetesObjectsProvider } from '@backstage/plugin-kubernetes-node'; import { KubernetesRequestAuth } from '@backstage/plugin-kubernetes-common'; import type { KubernetesRequestBody } from '@backstage/plugin-kubernetes-common'; +import { KubernetesServiceLocator } from '@backstage/plugin-kubernetes-node'; import { Logger } from 'winston'; +import { ObjectToFetch } from '@backstage/plugin-kubernetes-node'; import { PermissionEvaluator } from '@backstage/plugin-permission-common'; import { PluginEndpointDiscovery } from '@backstage/backend-common'; import { RequestHandler } from 'http-proxy-middleware'; @@ -42,19 +46,9 @@ export class AnonymousStrategy implements AuthenticationStrategy { validateCluster(): Error[]; } -// @public (undocumented) -export interface AuthenticationStrategy { - // (undocumented) - getCredential( - clusterDetails: ClusterDetails, - authConfig: KubernetesRequestAuth, - ): Promise; - // (undocumented) - validateCluster(authMetadata: AuthMetadata): Error[]; -} +export { AuthenticationStrategy }; -// @public -export type AuthMetadata = Record; +export { AuthMetadata }; // @public (undocumented) export class AwsIamStrategy implements AuthenticationStrategy { @@ -74,35 +68,11 @@ export class AzureIdentityStrategy implements AuthenticationStrategy { validateCluster(): Error[]; } -// @public (undocumented) -export interface ClusterDetails { - // (undocumented) - authMetadata: AuthMetadata; - // (undocumented) - caData?: string | undefined; - // (undocumented) - caFile?: string | undefined; - customResources?: CustomResourceMatcher[]; - dashboardApp?: string; - dashboardParameters?: JsonObject; - dashboardUrl?: string; - name: string; - skipMetricsLookup?: boolean; - // (undocumented) - skipTLSVerify?: boolean; - // (undocumented) - url: string; -} +export { ClusterDetails }; // @public @deprecated export function createRouter(options: RouterOptions): Promise; -// @public (undocumented) -export interface CustomResource extends ObjectToFetch { - // (undocumented) - objectType: 'customresources'; -} - export { CustomResourcesByEntity }; // @public (undocumented) @@ -127,14 +97,6 @@ export type DispatchStrategyOptions = { }; }; -// @public (undocumented) -export interface FetchResponseWrapper { - // (undocumented) - errors: KubernetesFetchError[]; - // (undocumented) - responses: FetchResponse[]; -} - // @public (undocumented) export class GoogleServiceAccountStrategy implements AuthenticationStrategy { // (undocumented) @@ -276,20 +238,9 @@ export type KubernetesBuilderReturn = Promise<{ }; }>; -// @public -export interface KubernetesClustersSupplier { - getClusters(): Promise; -} +export { KubernetesClustersSupplier }; -// @public -export type KubernetesCredential = - | { - type: 'bearer token'; - token: string; - } - | { - type: 'anonymous'; - }; +export { KubernetesCredential }; // @public (undocumented) export interface KubernetesEnvironment { @@ -303,21 +254,6 @@ export interface KubernetesEnvironment { permissions: PermissionEvaluator; } -// @public -export interface KubernetesFetcher { - // (undocumented) - fetchObjectsForService( - params: ObjectFetchParams, - ): Promise; - // (undocumented) - fetchPodMetricsByNamespaces( - clusterDetails: ClusterDetails, - credential: KubernetesCredential, - namespaces: Set, - labelSelector?: string, - ): Promise; -} - export { KubernetesObjectsByEntity }; export { KubernetesObjectsProvider }; @@ -338,23 +274,6 @@ export interface KubernetesObjectsProviderOptions { serviceLocator: KubernetesServiceLocator; } -// @public (undocumented) -export type KubernetesObjectTypes = - | 'pods' - | 'services' - | 'configmaps' - | 'deployments' - | 'limitranges' - | 'resourcequotas' - | 'replicasets' - | 'horizontalpodautoscalers' - | 'jobs' - | 'cronjobs' - | 'ingresses' - | 'customresources' - | 'statefulsets' - | 'daemonsets'; - // @public export class KubernetesProxy { constructor(options: KubernetesProxyOptions); @@ -376,50 +295,9 @@ export type KubernetesProxyOptions = { authStrategy: AuthenticationStrategy; }; -// @public -export interface KubernetesServiceLocator { - // (undocumented) - getClustersByEntity( - entity: Entity, - requestContext: ServiceLocatorRequestContext, - ): Promise<{ - clusters: ClusterDetails[]; - }>; -} - -// @public (undocumented) -export interface ObjectFetchParams { - // (undocumented) - clusterDetails: ClusterDetails; - // (undocumented) - credential: KubernetesCredential; - // (undocumented) - customResources: CustomResource[]; - // (undocumented) - labelSelector?: string; - // (undocumented) - namespace?: string; - // (undocumented) - objectTypesToFetch: Set; - // (undocumented) - serviceId: string; -} - // @public (undocumented) export type ObjectsByEntityRequest = KubernetesRequestBody; -// @public (undocumented) -export interface ObjectToFetch { - // (undocumented) - apiVersion: string; - // (undocumented) - group: string; - // (undocumented) - objectType: KubernetesObjectTypes; - // (undocumented) - plural: string; -} - // @public (undocumented) export class OidcStrategy implements AuthenticationStrategy { // (undocumented) @@ -458,18 +336,12 @@ export class ServiceAccountStrategy implements AuthenticationStrategy { // @public (undocumented) export type ServiceLocatorMethod = 'multiTenant' | 'singleTenant' | 'http'; -// @public (undocumented) -export interface ServiceLocatorRequestContext { - // (undocumented) - customResources: CustomResourceMatcher[]; - // (undocumented) - objectTypesToFetch: Set; -} - // @public (undocumented) export type SigningCreds = { accessKeyId: string | undefined; secretAccessKey: string | undefined; sessionToken: string | undefined; }; + +export * from '@backstage/plugin-kubernetes-node'; ``` diff --git a/plugins/kubernetes-backend/src/auth/types.ts b/plugins/kubernetes-backend/src/auth/types.ts index 611835048b..c7d60e8f2b 100644 --- a/plugins/kubernetes-backend/src/auth/types.ts +++ b/plugins/kubernetes-backend/src/auth/types.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +// TODO remove this re-export as a breaking change after a couple of releases + export type { AuthenticationStrategy, KubernetesCredential, diff --git a/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts b/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts index c676cc6246..77d02a032a 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts @@ -14,106 +14,181 @@ * limitations under the License. */ -import { getVoidLogger } from '@backstage/backend-common'; import { Entity } from '@backstage/catalog-model'; -import { Config, ConfigReader } from '@backstage/config'; import { ANNOTATION_KUBERNETES_AUTH_PROVIDER, ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER, ObjectsByEntityResponse, KubernetesRequestAuth, } from '@backstage/plugin-kubernetes-common'; -import express from 'express'; import request from 'supertest'; import { ClusterDetails, FetchResponseWrapper, - KubernetesClustersSupplier, KubernetesFetcher, KubernetesServiceLocator, ObjectFetchParams, } from '../types/types'; import { KubernetesCredential } from '../auth/types'; -import { KubernetesBuilder } from './KubernetesBuilder'; -import { KubernetesFanOutHandler } from './KubernetesFanOutHandler'; -import { CatalogApi } from '@backstage/catalog-client'; import { HEADER_KUBERNETES_CLUSTER, HEADER_KUBERNETES_AUTH, } from './KubernetesProxy'; import { setupServer } from 'msw/node'; -import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { + ServiceMock, + mockServices, + setupRequestMockHandlers, + startTestBackend, +} from '@backstage/backend-test-utils'; import { rest } from 'msw'; import { AuthorizeResult, PermissionEvaluator, } from '@backstage/plugin-permission-common'; +import { + PermissionsService, + createBackendModule, +} from '@backstage/backend-plugin-api'; +import { + KubernetesObjectsProvider, + kubernetesAuthStrategyExtensionPoint, + kubernetesClusterSupplierExtensionPoint, + kubernetesObjectsProviderExtensionPoint, + kubernetesFetcherExtensionPoint, + kubernetesServiceLocatorExtensionPoint, +} from '@backstage/plugin-kubernetes-node'; +import { ExtendedHttpServer } from '@backstage/backend-app-api'; describe('KubernetesBuilder', () => { - let app: express.Express; - let kubernetesFanOutHandler: jest.Mocked; - let config: Config; - let catalogApi: CatalogApi; - let permissions: jest.Mocked; + let app: ExtendedHttpServer; + let objectsProviderMock: KubernetesObjectsProvider; + const happyK8SResult = { + items: [ + { + clusterOne: { + pods: [ + { + metadata: { + name: 'pod1', + }, + }, + ], + }, + }, + ], + } as any; + const policyMock: jest.Mocked = { + authorize: jest.fn(), + authorizeConditional: jest.fn(), + }; + const permissionsMock: ServiceMock = + mockServices.permissions.mock(policyMock); beforeEach(async () => { jest.resetAllMocks(); - const logger = getVoidLogger(); - config = new ConfigReader({ - kubernetes: { - serviceLocatorMethod: { type: 'multiTenant' }, - clusterLocatorMethods: [{ type: 'config', clusters: [] }], - }, + + objectsProviderMock = { + getKubernetesObjectsByEntity: jest.fn().mockImplementation(_ => { + return Promise.resolve(happyK8SResult); + }), + getCustomResourcesByEntity: jest.fn().mockImplementation(_ => { + return Promise.resolve(happyK8SResult); + }), + }; + + const clusterSupplierMock = { + getClusters: jest.fn().mockImplementation(_ => { + return Promise.resolve([ + { + name: 'some-cluster', + url: 'https://localhost:1234', + authMetadata: { + [ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'serviceAccount', + }, + }, + { + name: 'some-other-cluster', + url: 'https://localhost:1235', + authMetadata: { + [ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'oidc', + [ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER]: 'google', + }, + }, + ]); + }), + }; + + jest.mock('@backstage/catalog-client', () => ({ + CatalogClient: jest.fn().mockImplementation(() => ({ + getEntityByRef: jest.fn().mockImplementation(entityRef => { + if (entityRef.name === 'noentity') { + return Promise.resolve(undefined); + } + return Promise.resolve({ + kind: entityRef.kind, + metadata: { + name: entityRef.name, + namespace: entityRef.namespace, + }, + } as Entity); + }), + })), + })); + + const { server } = await startTestBackend({ + features: [ + mockServices.rootConfig.factory({ + data: { + kubernetes: { + serviceLocatorMethod: { + type: 'multiTenant', + }, + clusterLocatorMethods: [ + { + type: 'config', + clusters: [], + }, + ], + }, + }, + }), + import('@backstage/plugin-kubernetes-backend/alpha'), + import('@backstage/plugin-permission-backend/alpha'), + import('@backstage/plugin-permission-backend-module-allow-all-policy'), + 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); + }, + }); + }, + }), + ], }); - const clusters: ClusterDetails[] = [ - { - name: 'some-cluster', - url: 'https://localhost:1234', - authMetadata: { - [ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'serviceAccount', - }, - }, - { - name: 'some-other-cluster', - url: 'https://localhost:1235', - authMetadata: { - [ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'oidc', - [ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER]: 'google', - }, - }, - ]; - const clusterSupplier: KubernetesClustersSupplier = { - async getClusters() { - return clusters; - }, - }; - - kubernetesFanOutHandler = { - getKubernetesObjectsByEntity: jest.fn(), - } as any; - - permissions = { - authorize: jest.fn(), - authorizeConditional: jest.fn(), - }; - - const { router } = await KubernetesBuilder.createBuilder({ - config, - logger, - catalogApi, - permissions, - }) - .setObjectsProvider(kubernetesFanOutHandler) - .setClusterSupplier(clusterSupplier) - .build(); - - app = express().use(router); + app = server; }); describe('get /clusters', () => { it('happy path: lists clusters', async () => { - const response = await request(app).get('/clusters'); + const response = await request(app).get('/api/kubernetes/clusters'); expect(response.status).toEqual(200); expect(response.body).toStrictEqual({ @@ -133,70 +208,41 @@ describe('KubernetesBuilder', () => { }); describe('post /services/:serviceId', () => { it('happy path: lists kubernetes objects without auth in request body', async () => { - const result = { - clusterOne: { - pods: [ - { - metadata: { - name: 'pod1', - }, - }, - ], - }, - } as any; - kubernetesFanOutHandler.getKubernetesObjectsByEntity.mockReturnValueOnce( - Promise.resolve(result), + const response = await request(app).post( + '/api/kubernetes/services/test-service', ); - - const response = await request(app).post('/services/test-service'); - expect(response.status).toEqual(200); - expect(response.body).toEqual(result); + expect(response.body).toEqual(happyK8SResult); }); it('happy path: lists kubernetes objects with auth in request body', async () => { - const result = { - clusterOne: { - pods: [ - { - metadata: { - name: 'pod1', - }, - }, - ], - }, - } as any; - kubernetesFanOutHandler.getKubernetesObjectsByEntity.mockReturnValueOnce( - Promise.resolve(result), - ); - const response = await request(app) - .post('/services/test-service') + .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(result); + expect(response.body).toEqual(happyK8SResult); }); it('internal error: lists kubernetes objects', async () => { - kubernetesFanOutHandler.getKubernetesObjectsByEntity.mockRejectedValue( - Error('some internal error'), - ); + objectsProviderMock.getKubernetesObjectsByEntity = jest + .fn() + .mockRejectedValue(Error('some internal error')); - const response = await request(app).post('/services/test-service'); + 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 logger = getVoidLogger(); - const someCluster: ClusterDetails = { + const someCluster = { name: 'some-cluster', url: 'https://localhost:1234', authMetadata: { @@ -212,11 +258,13 @@ describe('KubernetesBuilder', () => { authMetadata: { [ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'google' }, }, ]; - const clusterSupplier: KubernetesClustersSupplier = { - async getClusters() { - return clusters; - }, + + const clusterSupplierMock = { + getClusters: jest.fn().mockImplementation(_ => { + return Promise.resolve(clusters); + }), }; + const pod = { metadata: { name: 'pod1', @@ -240,7 +288,7 @@ describe('KubernetesBuilder', () => { ], }; - const serviceLocator: KubernetesServiceLocator = { + const mockServiceLocator: KubernetesServiceLocator = { getClustersByEntity( _entity: Entity, ): Promise<{ clusters: ClusterDetails[] }> { @@ -248,7 +296,7 @@ describe('KubernetesBuilder', () => { }, }; - const fetcher: KubernetesFetcher = { + const mockFetcher: KubernetesFetcher = { fetchPodMetricsByNamespaces( _clusterDetails: ClusterDetails, _credential: KubernetesCredential, @@ -271,20 +319,67 @@ describe('KubernetesBuilder', () => { }, }; - const { router } = await KubernetesBuilder.createBuilder({ - logger, - config, - catalogApi, - permissions, - }) - .setClusterSupplier(clusterSupplier) - .setServiceLocator(serviceLocator) - .setFetcher(fetcher) - .build(); - app = express().use(router); + 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: 'testClusterSupplier', + register(env) { + env.registerInit({ + deps: { extension: kubernetesClusterSupplierExtensionPoint }, + async init({ extension }) { + extension.addClusterSupplier(clusterSupplierMock); + }, + }); + }, + }), + 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('/services/test-service') + .post('/api/kubernetes/services/test-service') .send({ entity: { metadata: { @@ -298,9 +393,6 @@ describe('KubernetesBuilder', () => { }); it('reads auth data for custom strategy', async () => { - permissions.authorize.mockResolvedValue([ - { result: AuthorizeResult.ALLOW }, - ]); const mockFetcher = { fetchPodMetricsByNamespaces: jest .fn() @@ -312,43 +404,93 @@ describe('KubernetesBuilder', () => { ], }), }; - const { router } = await KubernetesBuilder.createBuilder({ - logger: getVoidLogger(), - config, - catalogApi, - permissions, - }) - .addAuthStrategy('custom', { - getCredential: jest - .fn< - Promise, - [ClusterDetails, KubernetesRequestAuth] - >() - .mockImplementation(async (_, requestAuth) => ({ - type: 'bearer token', - token: requestAuth.custom as string, - })), - validateCluster: jest.fn().mockReturnValue([]), - }) - .setClusterSupplier({ - getClusters: jest - .fn, []>() - .mockResolvedValue([ - { - name: 'custom-cluster', - url: 'http://my.cluster.url', - authMetadata: { - [ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'custom', - }, + + const clusterSupplierMock = { + getClusters: jest.fn().mockImplementation(_ => { + return Promise.resolve([ + { + name: 'custom-cluster', + url: 'http://my.cluster.url', + authMetadata: { + [ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'custom', }, - ]), - }) - .setFetcher(mockFetcher) - .build(); - app = express().use(router); + }, + ]); + }), + }; + + 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: 'testClusterSupplier', + register(env) { + env.registerInit({ + deps: { extension: kubernetesClusterSupplierExtensionPoint }, + async init({ extension }) { + extension.addClusterSupplier(clusterSupplierMock); + }, + }); + }, + }), + 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([]), + }); + }, + }); + }, + }), + createBackendModule({ + pluginId: 'kubernetes', + moduleId: 'testFetcher', + register(env) { + env.registerInit({ + deps: { extension: kubernetesFetcherExtensionPoint }, + async init({ extension }) { + extension.addFetcher(mockFetcher); + }, + }); + }, + }), + ], + }); + + app = server; await request(app) - .post('/services/test-service') + .post('/api/kubernetes/services/test-service') .send({ entity: { metadata: { @@ -400,12 +542,8 @@ describe('KubernetesBuilder', () => { }, }; - permissions.authorize.mockReturnValue( - Promise.resolve([{ result: AuthorizeResult.ALLOW }]), - ); - const proxyEndpointRequest = request(app) - .post('/proxy/api/v1/namespaces') + .post('/api/kubernetes/proxy/api/v1/namespaces') .set(HEADER_KUBERNETES_CLUSTER, 'some-cluster') .set(HEADER_KUBERNETES_AUTH, 'randomtoken') .send(requestBody); @@ -425,12 +563,8 @@ metadata: name: new-ns `; - permissions.authorize.mockReturnValue( - Promise.resolve([{ result: AuthorizeResult.ALLOW }]), - ); - const proxyEndpointRequest = request(app) - .post('/proxy/api/v1/namespaces') + .post('/api/kubernetes/proxy/api/v1/namespaces') .set(HEADER_KUBERNETES_CLUSTER, 'some-cluster') .set(HEADER_KUBERNETES_AUTH, 'randomtoken') .set('content-type', 'application/yaml') @@ -451,12 +585,35 @@ metadata: }, }; - permissions.authorize.mockReturnValue( - Promise.resolve([{ result: AuthorizeResult.DENY }]), - ); + permissionsMock.authorize.mockResolvedValue([ + { result: AuthorizeResult.DENY }, + ]); - const proxyEndpointRequest = request(app) - .post('/proxy/api/v1/namespaces') + const { server } = await startTestBackend({ + features: [ + mockServices.rootConfig.factory({ + data: { + kubernetes: { + serviceLocatorMethod: { + type: 'multiTenant', + }, + clusterLocatorMethods: [ + { + type: 'config', + clusters: [], + }, + ], + }, + }, + }), + permissionsMock.factory, + import('@backstage/plugin-kubernetes-backend/alpha'), + // import('@backstage/plugin-permission-backend/alpha'), + ], + }); + + const proxyEndpointRequest = request(server) + .post('/api/kubernetes/proxy/api/v1/namespaces') .set(HEADER_KUBERNETES_CLUSTER, 'some-cluster') .set(HEADER_KUBERNETES_AUTH, 'randomtoken') .send(requestBody); @@ -477,42 +634,90 @@ metadata: return res(ctx.json({ items: [] })); }), ); - permissions.authorize.mockResolvedValue([ - { result: AuthorizeResult.ALLOW }, - ]); - const { router } = await KubernetesBuilder.createBuilder({ - logger: getVoidLogger(), - config, - catalogApi, - permissions, - }) - .addAuthStrategy('custom', { - getCredential: jest - .fn< - Promise, - [ClusterDetails, KubernetesRequestAuth] - >() - .mockResolvedValue({ type: 'anonymous' }), - validateCluster: jest.fn().mockReturnValue([]), - }) - .setClusterSupplier({ - getClusters: jest - .fn, []>() - .mockResolvedValue([ - { - name: 'custom-cluster', - url: 'http://my.cluster.url', - authMetadata: { - [ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'custom', - }, + + const clusterSupplierMock = { + getClusters: jest.fn().mockImplementation(_ => { + return Promise.resolve([ + { + name: 'custom-cluster', + url: 'http://my.cluster.url', + authMetadata: { + [ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'custom', }, - ]), - }) - .build(); - app = express().use(router); + }, + ]); + }), + }; + + 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', + 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([]), + }); + }, + }); + }, + }), + ], + }); + + app = server; const proxyEndpointRequest = request(app) - .get('/proxy/api/v1/namespaces') + .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())); @@ -522,29 +727,44 @@ metadata: }); it('should not permit custom auth strategies with dashes', async () => { - const throwError = () => - KubernetesBuilder.createBuilder({ - logger: getVoidLogger(), - config, - catalogApi, - permissions, - }).addAuthStrategy('custom-strategy', { - getCredential: jest - .fn< - Promise, - [ClusterDetails, KubernetesRequestAuth] - >() - .mockResolvedValue({ type: 'anonymous' }), - validateCluster: jest.fn().mockReturnValue([]), + const throwError = async () => { + await 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([]), + }); + }, + }); + }, + }), + ], }); + }; - expect(throwError).toThrow('Strategy name can not include dashes'); + await expect(throwError).rejects.toThrow( + 'Strategy name can not include dashes', + ); }); }); + describe('get /.well-known/backstage/permissions/metadata', () => { it('lists permissions supported by the kubernetes plugin', async () => { const response = await request(app).get( - '/.well-known/backstage/permissions/metadata', + '/api/kubernetes/.well-known/backstage/permissions/metadata', ); expect(response.status).toEqual(200); diff --git a/plugins/kubernetes-backend/src/types/types.ts b/plugins/kubernetes-backend/src/types/types.ts index fade501417..18d1dc35c9 100644 --- a/plugins/kubernetes-backend/src/types/types.ts +++ b/plugins/kubernetes-backend/src/types/types.ts @@ -24,28 +24,6 @@ import { ObjectToFetch, } from '@backstage/plugin-kubernetes-node'; -/** - * - * @public - */ -export type KubernetesObjectTypes = - | 'pods' - | 'services' - | 'configmaps' - | 'deployments' - | 'limitranges' - | 'resourcequotas' - | 'replicasets' - | 'horizontalpodautoscalers' - | 'jobs' - | 'cronjobs' - | 'ingresses' - | 'customresources' - | 'statefulsets' - | 'daemonsets'; -// If updating this list, also make sure to update -// `objectTypes` and `apiVersionOverrides` in config.d.ts! - /** * * @public @@ -71,4 +49,5 @@ export interface KubernetesObjectsProviderOptions { */ export type ObjectsByEntityRequest = KubernetesRequestBody; +// TODO remove this re-export as a breaking change after a couple of releases export type * from '@backstage/plugin-kubernetes-node'; diff --git a/plugins/kubernetes-node/api-report.md b/plugins/kubernetes-node/api-report.md index d2981b32d1..2272035742 100644 --- a/plugins/kubernetes-node/api-report.md +++ b/plugins/kubernetes-node/api-report.md @@ -3,19 +3,131 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { AuthenticationStrategy as AuthenticationStrategy_2 } from '@backstage/plugin-kubernetes-node'; import { CustomResourceMatcher } from '@backstage/plugin-kubernetes-common'; import { Entity } from '@backstage/catalog-model'; import { ExtensionPoint } from '@backstage/backend-plugin-api'; +import { FetchResponse } from '@backstage/plugin-kubernetes-common'; +import { JsonObject } from '@backstage/types'; +import { KubernetesClustersSupplier as KubernetesClustersSupplier_2 } from '@backstage/plugin-kubernetes-node'; +import { KubernetesFetcher as KubernetesFetcher_2 } from '@backstage/plugin-kubernetes-node'; +import { KubernetesFetchError } from '@backstage/plugin-kubernetes-common'; import { KubernetesObjectsProvider as KubernetesObjectsProvider_2 } from '@backstage/plugin-kubernetes-node'; import { KubernetesRequestAuth } from '@backstage/plugin-kubernetes-common'; +import { KubernetesServiceLocator as KubernetesServiceLocator_2 } from '@backstage/plugin-kubernetes-node'; import { ObjectsByEntityResponse } from '@backstage/plugin-kubernetes-common'; +// @public (undocumented) +export interface AuthenticationStrategy { + // (undocumented) + getCredential( + clusterDetails: ClusterDetails, + authConfig: KubernetesRequestAuth, + ): Promise; + // (undocumented) + validateCluster(authMetadata: AuthMetadata): Error[]; +} + +// @public +export type AuthMetadata = Record; + +// @public (undocumented) +export interface ClusterDetails { + // (undocumented) + authMetadata: AuthMetadata; + // (undocumented) + caData?: string | undefined; + // (undocumented) + caFile?: string | undefined; + customResources?: CustomResourceMatcher[]; + dashboardApp?: string; + dashboardParameters?: JsonObject; + dashboardUrl?: string; + name: string; + skipMetricsLookup?: boolean; + // (undocumented) + skipTLSVerify?: boolean; + // (undocumented) + url: string; +} + +// @public (undocumented) +export interface CustomResource extends ObjectToFetch { + // (undocumented) + objectType: 'customresources'; +} + // @public (undocumented) export interface CustomResourcesByEntity extends KubernetesObjectsByEntity { // (undocumented) customResources: CustomResourceMatcher[]; } +// @public (undocumented) +export interface FetchResponseWrapper { + // (undocumented) + errors: KubernetesFetchError[]; + // (undocumented) + responses: FetchResponse[]; +} + +// @public +export interface KubernetesAuthStrategyExtensionPoint { + // (undocumented) + addAuthStrategy(key: string, strategy: AuthenticationStrategy_2): void; +} + +// @public +export const kubernetesAuthStrategyExtensionPoint: ExtensionPoint; + +// @public +export interface KubernetesClustersSupplier { + getClusters(): Promise; +} + +// @public +export interface KubernetesClusterSupplierExtensionPoint { + // (undocumented) + addClusterSupplier(clusterSupplier: KubernetesClustersSupplier_2): void; +} + +// @public +export const kubernetesClusterSupplierExtensionPoint: ExtensionPoint; + +// @public +export type KubernetesCredential = + | { + type: 'bearer token'; + token: string; + } + | { + type: 'anonymous'; + }; + +// @public +export interface KubernetesFetcher { + // (undocumented) + fetchObjectsForService( + params: ObjectFetchParams, + ): Promise; + // (undocumented) + fetchPodMetricsByNamespaces( + clusterDetails: ClusterDetails, + credential: KubernetesCredential, + namespaces: Set, + labelSelector?: string, + ): Promise; +} + +// @public +export interface KubernetesFetcherExtensionPoint { + // (undocumented) + addFetcher(fetcher: KubernetesFetcher_2): void; +} + +// @public +export const kubernetesFetcherExtensionPoint: ExtensionPoint; + // @public (undocumented) export interface KubernetesObjectsByEntity { // (undocumented) @@ -44,4 +156,79 @@ export interface KubernetesObjectsProviderExtensionPoint { // @public export const kubernetesObjectsProviderExtensionPoint: ExtensionPoint; + +// @public (undocumented) +export type KubernetesObjectTypes = + | 'pods' + | 'services' + | 'configmaps' + | 'deployments' + | 'limitranges' + | 'resourcequotas' + | 'replicasets' + | 'horizontalpodautoscalers' + | 'jobs' + | 'cronjobs' + | 'ingresses' + | 'customresources' + | 'statefulsets' + | 'daemonsets'; + +// @public +export interface KubernetesServiceLocator { + // (undocumented) + getClustersByEntity( + entity: Entity, + requestContext: ServiceLocatorRequestContext, + ): Promise<{ + clusters: ClusterDetails[]; + }>; +} + +// @public +export interface KubernetesServiceLocatorExtensionPoint { + // (undocumented) + addServiceLocator(serviceLocator: KubernetesServiceLocator_2): void; +} + +// @public +export const kubernetesServiceLocatorExtensionPoint: ExtensionPoint; + +// @public (undocumented) +export interface ObjectFetchParams { + // (undocumented) + clusterDetails: ClusterDetails; + // (undocumented) + credential: KubernetesCredential; + // (undocumented) + customResources: CustomResource[]; + // (undocumented) + labelSelector?: string; + // (undocumented) + namespace?: string; + // (undocumented) + objectTypesToFetch: Set; + // (undocumented) + serviceId: string; +} + +// @public (undocumented) +export interface ObjectToFetch { + // (undocumented) + apiVersion: string; + // (undocumented) + group: string; + // (undocumented) + objectType: KubernetesObjectTypes; + // (undocumented) + plural: string; +} + +// @public (undocumented) +export interface ServiceLocatorRequestContext { + // (undocumented) + customResources: CustomResourceMatcher[]; + // (undocumented) + objectTypesToFetch: Set; +} ``` diff --git a/plugins/kubernetes-node/package.json b/plugins/kubernetes-node/package.json index 37922fc2e9..56f4ba01c4 100644 --- a/plugins/kubernetes-node/package.json +++ b/plugins/kubernetes-node/package.json @@ -30,7 +30,6 @@ "dependencies": { "@backstage/backend-plugin-api": "workspace:^", "@backstage/catalog-model": "workspace:^", - "@backstage/plugin-kubernetes-backend": "workspace:^", "@backstage/plugin-kubernetes-common": "workspace:^", "@backstage/types": "workspace:^" } diff --git a/plugins/kubernetes-node/src/types/types.ts b/plugins/kubernetes-node/src/types/types.ts index 59582bc0f2..a0f48d0c26 100644 --- a/plugins/kubernetes-node/src/types/types.ts +++ b/plugins/kubernetes-node/src/types/types.ts @@ -14,7 +14,6 @@ * limitations under the License. */ import { Entity } from '@backstage/catalog-model'; -import { KubernetesObjectTypes } from '@backstage/plugin-kubernetes-backend'; import { CustomResourceMatcher, FetchResponse, @@ -154,6 +153,28 @@ export interface AuthenticationStrategy { validateCluster(authMetadata: AuthMetadata): Error[]; } +/** + * + * @public + */ +export type KubernetesObjectTypes = + | 'pods' + | 'services' + | 'configmaps' + | 'deployments' + | 'limitranges' + | 'resourcequotas' + | 'replicasets' + | 'horizontalpodautoscalers' + | 'jobs' + | 'cronjobs' + | 'ingresses' + | 'customresources' + | 'statefulsets' + | 'daemonsets'; +// If updating this list, also make sure to update +// `objectTypes` and `apiVersionOverrides` in config.d.ts on @backstage/plugin-kubernetes-backend! + /** * * @public diff --git a/yarn.lock b/yarn.lock index 71aa0872c4..4db4eb15e8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7674,7 +7674,6 @@ __metadata: "@backstage/backend-plugin-api": "workspace:^" "@backstage/catalog-model": "workspace:^" "@backstage/cli": "workspace:^" - "@backstage/plugin-kubernetes-backend": "workspace:^" "@backstage/plugin-kubernetes-common": "workspace:^" "@backstage/types": "workspace:^" languageName: unknown