diff --git a/.changeset/plenty-falcons-travel.md b/.changeset/plenty-falcons-travel.md new file mode 100644 index 0000000000..9e1f08246d --- /dev/null +++ b/.changeset/plenty-falcons-travel.md @@ -0,0 +1,12 @@ +--- +'@backstage/plugin-kubernetes-node': patch +'@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 . +`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..e6996484dd 100644 --- a/plugins/kubernetes-backend/api-report.md +++ b/plugins/kubernetes-backend/api-report.md @@ -3,21 +3,20 @@ > 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 { CatalogApi } from '@backstage/catalog-client'; +import { ClusterDetails as ClusterDetails_2 } from '@backstage/plugin-kubernetes-node'; import { Config } from '@backstage/config'; -import type { CustomResourceMatcher } from '@backstage/plugin-kubernetes-common'; -import { CustomResourcesByEntity } from '@backstage/plugin-kubernetes-node'; +import { CustomResource as CustomResource_2 } 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 { KubernetesObjectsByEntity } from '@backstage/plugin-kubernetes-node'; -import { KubernetesObjectsProvider } from '@backstage/plugin-kubernetes-node'; +import * as k8sAuthTypes from '@backstage/plugin-kubernetes-node'; +import { KubernetesClustersSupplier as KubernetesClustersSupplier_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 { Logger } from 'winston'; +import { ObjectToFetch as ObjectToFetch_2 } 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 +41,11 @@ export class AnonymousStrategy implements AuthenticationStrategy { validateCluster(): Error[]; } -// @public (undocumented) -export interface AuthenticationStrategy { - // (undocumented) - getCredential( - clusterDetails: ClusterDetails, - authConfig: KubernetesRequestAuth, - ): Promise; - // (undocumented) - validateCluster(authMetadata: AuthMetadata): Error[]; -} +// @public @deprecated (undocumented) +export type AuthenticationStrategy = k8sAuthTypes.AuthenticationStrategy; -// @public -export type AuthMetadata = Record; +// @public @deprecated (undocumented) +export type AuthMetadata = k8sAuthTypes.AuthMetadata; // @public (undocumented) export class AwsIamStrategy implements AuthenticationStrategy { @@ -74,36 +65,17 @@ 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; -} +// @public @deprecated (undocumented) +export type ClusterDetails = k8sAuthTypes.ClusterDetails; // @public @deprecated export function createRouter(options: RouterOptions): Promise; -// @public (undocumented) -export interface CustomResource extends ObjectToFetch { - // (undocumented) - objectType: 'customresources'; -} +// @public @deprecated (undocumented) +export type CustomResource = k8sAuthTypes.CustomResource; -export { CustomResourcesByEntity }; +// @public @deprecated (undocumented) +export type CustomResourcesByEntity = k8sAuthTypes.CustomResourcesByEntity; // @public (undocumented) export const DEFAULT_OBJECTS: ObjectToFetch[]; @@ -127,13 +99,8 @@ export type DispatchStrategyOptions = { }; }; -// @public (undocumented) -export interface FetchResponseWrapper { - // (undocumented) - errors: KubernetesFetchError[]; - // (undocumented) - responses: FetchResponse[]; -} +// @public @deprecated (undocumented) +export type FetchResponseWrapper = k8sAuthTypes.FetchResponseWrapper; // @public (undocumented) export class GoogleServiceAccountStrategy implements AuthenticationStrategy { @@ -169,37 +136,37 @@ export class KubernetesBuilder { build(): KubernetesBuilderReturn; // (undocumented) protected buildAuthStrategyMap(): { - [key: string]: AuthenticationStrategy; + [key: string]: AuthenticationStrategy_2; }; // (undocumented) protected buildClusterSupplier( refreshInterval: Duration, - ): KubernetesClustersSupplier; + ): KubernetesClustersSupplier_2; // (undocumented) - protected buildCustomResources(): CustomResource[]; + protected buildCustomResources(): CustomResource_2[]; // (undocumented) protected buildFetcher(): KubernetesFetcher; // (undocumented) protected buildHttpServiceLocator( - _clusterSupplier: KubernetesClustersSupplier, + _clusterSupplier: KubernetesClustersSupplier_2, ): KubernetesServiceLocator; // (undocumented) protected buildMultiTenantServiceLocator( - clusterSupplier: KubernetesClustersSupplier, + clusterSupplier: KubernetesClustersSupplier_2, ): KubernetesServiceLocator; // (undocumented) protected buildObjectsProvider( options: KubernetesObjectsProviderOptions, - ): KubernetesObjectsProvider; + ): KubernetesObjectsProvider_2; // (undocumented) protected buildProxy( logger: Logger, - clusterSupplier: KubernetesClustersSupplier, + clusterSupplier: KubernetesClustersSupplier_2, ): KubernetesProxy; // (undocumented) protected buildRouter( - objectsProvider: KubernetesObjectsProvider, - clusterSupplier: KubernetesClustersSupplier, + objectsProvider: KubernetesObjectsProvider_2, + clusterSupplier: KubernetesClustersSupplier_2, catalogApi: CatalogApi, proxy: KubernetesProxy, permissionApi: PermissionEvaluator, @@ -207,11 +174,11 @@ export class KubernetesBuilder { // (undocumented) protected buildServiceLocator( method: ServiceLocatorMethod, - clusterSupplier: KubernetesClustersSupplier, + clusterSupplier: KubernetesClustersSupplier_2, ): KubernetesServiceLocator; // (undocumented) protected buildSingleTenantServiceLocator( - clusterSupplier: KubernetesClustersSupplier, + clusterSupplier: KubernetesClustersSupplier_2, ): KubernetesServiceLocator; // (undocumented) static createBuilder(env: KubernetesEnvironment): KubernetesBuilder; @@ -219,26 +186,26 @@ export class KubernetesBuilder { protected readonly env: KubernetesEnvironment; // (undocumented) protected fetchClusterDetails( - clusterSupplier: KubernetesClustersSupplier, - ): Promise; + clusterSupplier: KubernetesClustersSupplier_2, + ): Promise; // (undocumented) protected getAuthStrategyMap(): { - [key: string]: AuthenticationStrategy; + [key: string]: AuthenticationStrategy_2; }; // (undocumented) - protected getClusterSupplier(): KubernetesClustersSupplier; + protected getClusterSupplier(): KubernetesClustersSupplier_2; // (undocumented) protected getFetcher(): KubernetesFetcher; // (undocumented) protected getObjectsProvider( options: KubernetesObjectsProviderOptions, - ): KubernetesObjectsProvider; + ): KubernetesObjectsProvider_2; // (undocumented) - protected getObjectTypesToFetch(): ObjectToFetch[] | undefined; + protected getObjectTypesToFetch(): ObjectToFetch_2[] | undefined; // (undocumented) protected getProxy( logger: Logger, - clusterSupplier: KubernetesClustersSupplier, + clusterSupplier: KubernetesClustersSupplier_2, ): KubernetesProxy; // (undocumented) protected getServiceLocator(): KubernetesServiceLocator; @@ -249,13 +216,13 @@ export class KubernetesBuilder { [key: string]: AuthenticationStrategy; }): void; // (undocumented) - setClusterSupplier(clusterSupplier?: KubernetesClustersSupplier): this; + setClusterSupplier(clusterSupplier?: KubernetesClustersSupplier_2): this; // (undocumented) setDefaultClusterRefreshInterval(refreshInterval: Duration): this; // (undocumented) setFetcher(fetcher?: KubernetesFetcher): this; // (undocumented) - setObjectsProvider(objectsProvider?: KubernetesObjectsProvider): this; + setObjectsProvider(objectsProvider?: KubernetesObjectsProvider_2): this; // (undocumented) setProxy(proxy?: KubernetesProxy): this; // (undocumented) @@ -265,31 +232,23 @@ export class KubernetesBuilder { // @public export type KubernetesBuilderReturn = Promise<{ router: express.Router; - clusterSupplier: KubernetesClustersSupplier; + clusterSupplier: KubernetesClustersSupplier_2; customResources: CustomResource[]; fetcher: KubernetesFetcher; proxy: KubernetesProxy; - objectsProvider: KubernetesObjectsProvider; + objectsProvider: KubernetesObjectsProvider_2; serviceLocator: KubernetesServiceLocator; authStrategyMap: { [key: string]: AuthenticationStrategy; }; }>; -// @public -export interface KubernetesClustersSupplier { - getClusters(): Promise; -} +// @public @deprecated (undocumented) +export type KubernetesClustersSupplier = + k8sAuthTypes.KubernetesClustersSupplier; -// @public -export type KubernetesCredential = - | { - type: 'bearer token'; - token: string; - } - | { - type: 'anonymous'; - }; +// @public @deprecated (undocumented) +export type KubernetesCredential = k8sAuthTypes.KubernetesCredential; // @public (undocumented) export interface KubernetesEnvironment { @@ -303,57 +262,30 @@ 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; -} +// @public @deprecated (undocumented) +export type KubernetesFetcher = k8sAuthTypes.KubernetesFetcher; -export { KubernetesObjectsByEntity }; - -export { KubernetesObjectsProvider }; +// @public @deprecated (undocumented) +export type KubernetesObjectsProvider = k8sAuthTypes.KubernetesObjectsProvider; // @public (undocumented) export interface KubernetesObjectsProviderOptions { // (undocumented) config: Config; // (undocumented) - customResources: CustomResource[]; + customResources: k8sAuthTypes.CustomResource[]; // (undocumented) - fetcher: KubernetesFetcher; + fetcher: k8sAuthTypes.KubernetesFetcher; // (undocumented) logger: Logger; // (undocumented) - objectTypesToFetch?: ObjectToFetch[]; + objectTypesToFetch?: k8sAuthTypes.ObjectToFetch[]; // (undocumented) - serviceLocator: KubernetesServiceLocator; + serviceLocator: k8sAuthTypes.KubernetesServiceLocator; } -// @public (undocumented) -export type KubernetesObjectTypes = - | 'pods' - | 'services' - | 'configmaps' - | 'deployments' - | 'limitranges' - | 'resourcequotas' - | 'replicasets' - | 'horizontalpodautoscalers' - | 'jobs' - | 'cronjobs' - | 'ingresses' - | 'customresources' - | 'statefulsets' - | 'daemonsets'; +// @public @deprecated (undocumented) +export type KubernetesObjectTypes = k8sAuthTypes.KubernetesObjectTypes; // @public export class KubernetesProxy { @@ -376,49 +308,17 @@ export type KubernetesProxyOptions = { authStrategy: AuthenticationStrategy; }; -// @public -export interface KubernetesServiceLocator { - // (undocumented) - getClustersByEntity( - entity: Entity, - requestContext: ServiceLocatorRequestContext, - ): Promise<{ - clusters: ClusterDetails[]; - }>; -} +// @public @deprecated (undocumented) +export type KubernetesServiceLocator = k8sAuthTypes.KubernetesServiceLocator; -// @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 @deprecated (undocumented) +export type ObjectFetchParams = k8sAuthTypes.ObjectFetchParams; // @public (undocumented) export type ObjectsByEntityRequest = KubernetesRequestBody; -// @public (undocumented) -export interface ObjectToFetch { - // (undocumented) - apiVersion: string; - // (undocumented) - group: string; - // (undocumented) - objectType: KubernetesObjectTypes; - // (undocumented) - plural: string; -} +// @public @deprecated (undocumented) +export type ObjectToFetch = k8sAuthTypes.ObjectToFetch; // @public (undocumented) export class OidcStrategy implements AuthenticationStrategy { @@ -458,13 +358,9 @@ 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 @deprecated (undocumented) +export type ServiceLocatorRequestContext = + k8sAuthTypes.ServiceLocatorRequestContext; // @public (undocumented) export type SigningCreds = { diff --git a/plugins/kubernetes-backend/package.json b/plugins/kubernetes-backend/package.json index 64f308b74c..8a0a5cbefb 100644 --- a/plugins/kubernetes-backend/package.json +++ b/plugins/kubernetes-backend/package.json @@ -90,6 +90,8 @@ "@backstage/backend-app-api": "workspace:^", "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", + "@backstage/plugin-permission-backend": "workspace:^", + "@backstage/plugin-permission-backend-module-allow-all-policy": "workspace:^", "@types/aws4": "^1.5.1", "msw": "^1.0.0", "supertest": "^6.1.3", diff --git a/plugins/kubernetes-backend/src/auth/types.ts b/plugins/kubernetes-backend/src/auth/types.ts index fff9952bae..9b33265448 100644 --- a/plugins/kubernetes-backend/src/auth/types.ts +++ b/plugins/kubernetes-backend/src/auth/types.ts @@ -14,25 +14,15 @@ * limitations under the License. */ -import { AuthMetadata, ClusterDetails } from '../types/types'; -import { KubernetesRequestAuth } from '@backstage/plugin-kubernetes-common'; +import * as k8sAuthTypes from '@backstage/plugin-kubernetes-node'; + +// TODO remove this re-export as a breaking change after a couple of releases +/** + * @public @deprecated Import it from \@backstage/plugin-kubernetes-node instead + */ +export type AuthenticationStrategy = k8sAuthTypes.AuthenticationStrategy; /** - * Authentication data used to make a request to Kubernetes - * @public + * @public @deprecated Import it from \@backstage/plugin-kubernetes-node instead */ -export type KubernetesCredential = - | { type: 'bearer token'; token: string } - | { type: 'anonymous' }; - -/** - * - * @public - */ -export interface AuthenticationStrategy { - getCredential( - clusterDetails: ClusterDetails, - authConfig: KubernetesRequestAuth, - ): Promise; - validateCluster(authMetadata: AuthMetadata): Error[]; -} +export type KubernetesCredential = k8sAuthTypes.KubernetesCredential; diff --git a/plugins/kubernetes-backend/src/plugin.ts b/plugins/kubernetes-backend/src/plugin.ts index 83dbae15a0..1b4aca4160 100644 --- a/plugins/kubernetes-backend/src/plugin.ts +++ b/plugins/kubernetes-backend/src/plugin.ts @@ -26,6 +26,20 @@ import { KubernetesObjectsProviderExtensionPoint, kubernetesObjectsProviderExtensionPoint, KubernetesObjectsProvider, + KubernetesClusterSupplierExtensionPoint, + kubernetesClusterSupplierExtensionPoint, + KubernetesClustersSupplier, + KubernetesAuthStrategyExtensionPoint, + AuthenticationStrategy, + kubernetesAuthStrategyExtensionPoint, + KubernetesFetcher, + KubernetesServiceLocatorExtensionPoint, + KubernetesServiceLocator, + kubernetesServiceLocatorExtensionPoint, +} from '@backstage/plugin-kubernetes-node'; +import { + KubernetesFetcherExtensionPoint, + kubernetesFetcherExtensionPoint, } from '@backstage/plugin-kubernetes-node'; class ObjectsProvider implements KubernetesObjectsProviderExtensionPoint { @@ -45,6 +59,86 @@ class ObjectsProvider implements KubernetesObjectsProviderExtensionPoint { } } +class ClusterSuplier implements KubernetesClusterSupplierExtensionPoint { + private clusterSupplier: KubernetesClustersSupplier | undefined; + + getClusterSupplier() { + return this.clusterSupplier; + } + + addClusterSupplier(clusterSupplier: KubernetesClustersSupplier) { + if (this.clusterSupplier) { + throw new Error( + 'Multiple Kubernetes Cluster Suppliers is not supported at this time', + ); + } + this.clusterSupplier = clusterSupplier; + } +} + +class Fetcher implements KubernetesFetcherExtensionPoint { + private fetcher: KubernetesFetcher | undefined; + + getFetcher() { + return this.fetcher; + } + + addFetcher(fetcher: KubernetesFetcher) { + if (this.fetcher) { + throw new Error( + 'Multiple Kubernetes Fetchers is not supported at this time', + ); + } + this.fetcher = fetcher; + } +} + +class ServiceLocator implements KubernetesServiceLocatorExtensionPoint { + private serviceLocator: KubernetesServiceLocator | undefined; + + getServiceLocator() { + return this.serviceLocator; + } + + addServiceLocator(serviceLocator: KubernetesServiceLocator) { + if (this.serviceLocator) { + throw new Error( + 'Multiple Kubernetes Service Locators is not supported at this time', + ); + } + this.serviceLocator = serviceLocator; + } +} + +class AuthStrategy implements KubernetesAuthStrategyExtensionPoint { + private authStrategies: Array<{ + key: string; + strategy: AuthenticationStrategy; + }>; + + constructor() { + this.authStrategies = new Array<{ + key: string; + strategy: AuthenticationStrategy; + }>(); + } + + static addAuthStrategiesFromArray( + authStrategies: Array<{ key: string; strategy: AuthenticationStrategy }>, + builder: KubernetesBuilder, + ) { + authStrategies.forEach(st => builder.addAuthStrategy(st.key, st.strategy)); + } + + getAuthenticationStrategies() { + return this.authStrategies; + } + + addAuthStrategy(key: string, authStrategy: AuthenticationStrategy) { + this.authStrategies.push({ key, strategy: authStrategy }); + } +} + /** * This is the backend plugin that provides the Kubernetes integration. * @alpha @@ -53,10 +147,31 @@ class ObjectsProvider implements KubernetesObjectsProviderExtensionPoint { export const kubernetesPlugin = createBackendPlugin({ pluginId: 'kubernetes', register(env) { - const extensionPoint = new ObjectsProvider(); + const extPointObjectsProvider = new ObjectsProvider(); + const extPointClusterSuplier = new ClusterSuplier(); + const extPointAuthStrategy = new AuthStrategy(); + const extPointFetcher = new Fetcher(); + const extPointServiceLocator = new ServiceLocator(); + env.registerExtensionPoint( kubernetesObjectsProviderExtensionPoint, - extensionPoint, + extPointObjectsProvider, + ); + env.registerExtensionPoint( + kubernetesClusterSupplierExtensionPoint, + extPointClusterSuplier, + ); + env.registerExtensionPoint( + kubernetesAuthStrategyExtensionPoint, + extPointAuthStrategy, + ); + env.registerExtensionPoint( + kubernetesFetcherExtensionPoint, + extPointFetcher, + ); + env.registerExtensionPoint( + kubernetesServiceLocatorExtensionPoint, + extPointServiceLocator, ); env.registerInit({ @@ -70,14 +185,22 @@ export const kubernetesPlugin = createBackendPlugin({ async init({ http, logger, config, catalogApi, permissions }) { const winstonLogger = loggerToWinstonLogger(logger); // TODO: expose all of the customization & extension points of the builder here - const { router } = await KubernetesBuilder.createBuilder({ + const builder: KubernetesBuilder = KubernetesBuilder.createBuilder({ logger: winstonLogger, config, catalogApi, permissions, }) - .setObjectsProvider(extensionPoint.getObjectsProvider()) - .build(); + .setObjectsProvider(extPointObjectsProvider.getObjectsProvider()) + .setClusterSupplier(extPointClusterSuplier.getClusterSupplier()) + .setFetcher(extPointFetcher.getFetcher()) + .setServiceLocator(extPointServiceLocator.getServiceLocator()); + + AuthStrategy.addAuthStrategiesFromArray( + extPointAuthStrategy.getAuthenticationStrategies(), + builder, + ); + const { router } = await builder.build(); http.use(router); }, }); 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/service/KubernetesBuilder.ts b/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts index 3bc7402179..c1ff4688e7 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts @@ -46,7 +46,6 @@ import { MultiTenantServiceLocator } from '../service-locator/MultiTenantService import { SingleTenantServiceLocator } from '../service-locator/SingleTenantServiceLocator'; import { CustomResource, - KubernetesClustersSupplier, KubernetesFetcher, KubernetesObjectsProviderOptions, KubernetesObjectTypes, @@ -54,7 +53,10 @@ import { ObjectsByEntityRequest, ServiceLocatorMethod, } from '../types/types'; -import { KubernetesObjectsProvider } from '@backstage/plugin-kubernetes-node'; +import { + KubernetesClustersSupplier, + KubernetesObjectsProvider, +} from '@backstage/plugin-kubernetes-node'; import { DEFAULT_OBJECTS, KubernetesFanOutHandler, diff --git a/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.ts b/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.ts index 3ea06e061e..7a8ff1bf2d 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.ts @@ -17,7 +17,6 @@ import { Entity } from '@backstage/catalog-model'; import { Logger } from 'winston'; import { - ClusterDetails, KubernetesFetcher, KubernetesObjectsProviderOptions, KubernetesServiceLocator, @@ -26,7 +25,6 @@ import { ObjectToFetch, CustomResource, } from '../types/types'; -import { AuthenticationStrategy, KubernetesCredential } from '../auth/types'; import { ClientContainerStatus, ClientCurrentResourceUsage, @@ -45,7 +43,10 @@ import { PodStatus, } from '@kubernetes/client-node'; import { + AuthenticationStrategy, + ClusterDetails, CustomResourcesByEntity, + KubernetesCredential, KubernetesObjectsByEntity, } from '@backstage/plugin-kubernetes-node'; diff --git a/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts b/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts index d5c945a864..99501c48c6 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts @@ -26,12 +26,10 @@ import { import lodash, { Dictionary } from 'lodash'; import { Logger } from 'winston'; import { - ClusterDetails, FetchResponseWrapper, KubernetesFetcher, ObjectFetchParams, } from '../types/types'; -import { KubernetesCredential } from '../auth/types'; import { ANNOTATION_KUBERNETES_AUTH_PROVIDER, FetchResponse, @@ -43,6 +41,10 @@ import fetch, { RequestInit, Response } from 'node-fetch'; import * as https from 'https'; import fs from 'fs-extra'; import { JsonObject } from '@backstage/types'; +import { + ClusterDetails, + KubernetesCredential, +} from '@backstage/plugin-kubernetes-node'; export interface KubernetesClientBasedFetcherOptions { logger: Logger; diff --git a/plugins/kubernetes-backend/src/types/index.ts b/plugins/kubernetes-backend/src/types/index.ts index c8413c1aa9..db229eae34 100644 --- a/plugins/kubernetes-backend/src/types/index.ts +++ b/plugins/kubernetes-backend/src/types/index.ts @@ -15,9 +15,3 @@ */ export * from './types'; - -export type { - CustomResourcesByEntity, - KubernetesObjectsByEntity, - KubernetesObjectsProvider, -} from '@backstage/plugin-kubernetes-node'; diff --git a/plugins/kubernetes-backend/src/types/types.ts b/plugins/kubernetes-backend/src/types/types.ts index 6781dc1161..57a12bb58a 100644 --- a/plugins/kubernetes-backend/src/types/types.ts +++ b/plugins/kubernetes-backend/src/types/types.ts @@ -14,131 +14,10 @@ * limitations under the License. */ -import { Entity } from '@backstage/catalog-model'; import { Logger } from 'winston'; -import type { JsonObject } from '@backstage/types'; -import type { - CustomResourceMatcher, - FetchResponse, - KubernetesFetchError, - KubernetesRequestBody, -} from '@backstage/plugin-kubernetes-common'; +import type { KubernetesRequestBody } from '@backstage/plugin-kubernetes-common'; import { Config } from '@backstage/config'; -import { KubernetesCredential } from '../auth/types'; - -/** - * - * @public - */ -export interface ObjectFetchParams { - serviceId: string; - clusterDetails: ClusterDetails; - credential: KubernetesCredential; - objectTypesToFetch: Set; - labelSelector?: string; - customResources: CustomResource[]; - namespace?: string; -} - -/** - * Fetches information from a kubernetes cluster using the cluster details object to target a specific cluster - * - * @public - */ -export interface KubernetesFetcher { - fetchObjectsForService( - params: ObjectFetchParams, - ): Promise; - fetchPodMetricsByNamespaces( - clusterDetails: ClusterDetails, - credential: KubernetesCredential, - namespaces: Set, - labelSelector?: string, - ): Promise; -} - -/** - * - * @public - */ -export interface FetchResponseWrapper { - errors: KubernetesFetchError[]; - responses: FetchResponse[]; -} - -/** - * - * @public - */ -export interface ObjectToFetch { - objectType: KubernetesObjectTypes; - group: string; - apiVersion: string; - plural: string; -} - -/** - * - * @public - */ -export interface CustomResource extends ObjectToFetch { - objectType: 'customresources'; -} - -/** - * - * @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! - -/** - * Used to load cluster details from different sources - * @public - */ -export interface KubernetesClustersSupplier { - /** - * Returns the cached list of clusters. - * - * Implementations _should_ cache the clusters and refresh them periodically, - * as getClusters is called whenever the list of clusters is needed. - */ - getClusters(): Promise; -} - -/** - * @public - */ -export interface ServiceLocatorRequestContext { - objectTypesToFetch: Set; - customResources: CustomResourceMatcher[]; -} - -/** - * Used to locate which cluster(s) a service is running on - * @public - */ -export interface KubernetesServiceLocator { - getClustersByEntity( - entity: Entity, - requestContext: ServiceLocatorRequestContext, - ): Promise<{ clusters: ClusterDetails[] }>; -} +import * as k8sTypes from '@backstage/plugin-kubernetes-node'; /** * @@ -146,70 +25,6 @@ export interface KubernetesServiceLocator { */ export type ServiceLocatorMethod = 'multiTenant' | 'singleTenant' | 'http'; // TODO implement http -/** - * Provider-specific authentication configuration - * @public - */ -export type AuthMetadata = Record; - -/** - * - * @public - */ -export interface ClusterDetails { - /** - * Specifies the name of the Kubernetes cluster. - */ - name: string; - url: string; - authMetadata: AuthMetadata; - skipTLSVerify?: boolean; - /** - * Whether to skip the lookup to the metrics server to retrieve pod resource usage. - * It is not guaranteed that the Kubernetes distro has the metrics server installed. - */ - skipMetricsLookup?: boolean; - caData?: string | undefined; - caFile?: string | undefined; - /** - * Specifies the link to the Kubernetes dashboard managing this cluster. - * @remarks - * Note that you should specify the app used for the dashboard - * using the dashboardApp property, in order to properly format - * links to kubernetes resources, otherwise it will assume that you're running the standard one. - * @see dashboardApp - * @see dashboardParameters - */ - dashboardUrl?: string; - /** - * Specifies the app that provides the Kubernetes dashboard. - * This will be used for formatting links to kubernetes objects inside the dashboard. - * @remarks - * The existing apps are: standard, rancher, openshift, gke, aks, eks - * Note that it will default to the regular dashboard provided by the Kubernetes project (standard). - * Note that you can add your own formatter by registering it to the clusterLinksFormatters dictionary. - * @defaultValue standard - * @see dashboardUrl - * @example - * ```ts - * import { clusterLinksFormatters } from '@backstage/plugin-kubernetes'; - * clusterLinksFormatters.myDashboard = (options) => ...; - * ``` - */ - dashboardApp?: string; - /** - * Specifies specific parameters used by some dashboard URL formatters. - * This is used by the GKE formatter which requires the project, region and cluster name. - * @see dashboardApp - */ - dashboardParameters?: JsonObject; - /** - * Specifies which custom resources to look for when returning an entity's - * Kubernetes resources. - */ - customResources?: CustomResourceMatcher[]; -} - /** * * @public @@ -217,10 +32,10 @@ export interface ClusterDetails { export interface KubernetesObjectsProviderOptions { logger: Logger; config: Config; - fetcher: KubernetesFetcher; - serviceLocator: KubernetesServiceLocator; - customResources: CustomResource[]; - objectTypesToFetch?: ObjectToFetch[]; + fetcher: k8sTypes.KubernetesFetcher; + serviceLocator: k8sTypes.KubernetesServiceLocator; + customResources: k8sTypes.CustomResource[]; + objectTypesToFetch?: k8sTypes.ObjectToFetch[]; } /** @@ -228,3 +43,73 @@ export interface KubernetesObjectsProviderOptions { * @public */ export type ObjectsByEntityRequest = KubernetesRequestBody; + +// TODO remove this re-export as a breaking change after a couple of releases +/** + * @public @deprecated Import it from \@backstage/plugin-kubernetes-node instead + */ +export type KubernetesObjectsProvider = k8sTypes.KubernetesObjectsProvider; + +/** + * @public @deprecated Import it from \@backstage/plugin-kubernetes-node instead + */ +export type CustomResourcesByEntity = k8sTypes.CustomResourcesByEntity; + +/** + * @public + * @deprecated Import it from \@backstage/plugin-kubernetes-node instead + */ +export type AuthMetadata = k8sTypes.AuthMetadata; + +/** + * @public + * @deprecated Import it from \@backstage/plugin-kubernetes-node instead + */ +export type ClusterDetails = k8sTypes.ClusterDetails; + +/** + * @public + * @deprecated Import it from \@backstage/plugin-kubernetes-node instead + */ +export type KubernetesClustersSupplier = k8sTypes.KubernetesClustersSupplier; + +/** + * @public @deprecated Import it from \@backstage/plugin-kubernetes-node instead + */ +export type KubernetesObjectTypes = k8sTypes.KubernetesObjectTypes; + +/** + * @public @deprecated Import it from \@backstage/plugin-kubernetes-node instead + */ +export type ObjectToFetch = k8sTypes.ObjectToFetch; + +/** + * @public @deprecated Import it from \@backstage/plugin-kubernetes-node instead + */ +export type CustomResource = k8sTypes.CustomResource; + +/** + * @public @deprecated Import it from \@backstage/plugin-kubernetes-node instead + */ +export type ObjectFetchParams = k8sTypes.ObjectFetchParams; + +/** + * @public @deprecated Import it from \@backstage/plugin-kubernetes-node instead + */ +export type FetchResponseWrapper = k8sTypes.FetchResponseWrapper; + +/** + * @public @deprecated Import it from \@backstage/plugin-kubernetes-node instead + */ +export type KubernetesFetcher = k8sTypes.KubernetesFetcher; + +/** + * @public @deprecated Import it from \@backstage/plugin-kubernetes-node instead + */ +export type ServiceLocatorRequestContext = + k8sTypes.ServiceLocatorRequestContext; + +/** + * @public @deprecated Import it from \@backstage/plugin-kubernetes-node instead + */ +export type KubernetesServiceLocator = k8sTypes.KubernetesServiceLocator; 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 25e7a5da44..56f4ba01c4 100644 --- a/plugins/kubernetes-node/package.json +++ b/plugins/kubernetes-node/package.json @@ -30,6 +30,7 @@ "dependencies": { "@backstage/backend-plugin-api": "workspace:^", "@backstage/catalog-model": "workspace:^", - "@backstage/plugin-kubernetes-common": "workspace:^" + "@backstage/plugin-kubernetes-common": "workspace:^", + "@backstage/types": "workspace:^" } } diff --git a/plugins/kubernetes-node/src/extensions.ts b/plugins/kubernetes-node/src/extensions.ts index 152622cf96..d898244161 100644 --- a/plugins/kubernetes-node/src/extensions.ts +++ b/plugins/kubernetes-node/src/extensions.ts @@ -14,7 +14,13 @@ * limitations under the License. */ import { createExtensionPoint } from '@backstage/backend-plugin-api'; -import { KubernetesObjectsProvider } from '@backstage/plugin-kubernetes-node'; +import { + AuthenticationStrategy, + KubernetesClustersSupplier, + KubernetesFetcher, + KubernetesObjectsProvider, + KubernetesServiceLocator, +} from '@backstage/plugin-kubernetes-node'; /** * The interface for {@link kubernetesObjectsProviderExtensionPoint}. @@ -34,3 +40,79 @@ export const kubernetesObjectsProviderExtensionPoint = createExtensionPoint({ id: 'kubernetes.objects-provider', }); + +/** + * The interface for {@link kubernetesClusterSupplierExtensionPoint}. + * + * @public + */ +export interface KubernetesClusterSupplierExtensionPoint { + addClusterSupplier(clusterSupplier: KubernetesClustersSupplier): void; +} + +/** + * An extension point the exposes the ability to configure a cluster supplier. + * + * @public + */ +export const kubernetesClusterSupplierExtensionPoint = + createExtensionPoint({ + id: 'kubernetes.cluster-supplier', + }); + +/** + * The interface for {@link kubernetesAuthStrategyExtensionPoint}. + * + * @public + */ +export interface KubernetesAuthStrategyExtensionPoint { + addAuthStrategy(key: string, strategy: AuthenticationStrategy): void; +} + +/** + * An extension point the exposes the ability to add an Auth Strategy. + * + * @public + */ +export const kubernetesAuthStrategyExtensionPoint = + createExtensionPoint({ + id: 'kubernetes.auth-strategy', + }); + +/** + * The interface for {@link kubernetesFetcherExtensionPoint}. + * + * @public + */ +export interface KubernetesFetcherExtensionPoint { + addFetcher(fetcher: KubernetesFetcher): void; +} + +/** + * An extension point the exposes the ability to configure a kubernetes fetcher. + * + * @public + */ +export const kubernetesFetcherExtensionPoint = + createExtensionPoint({ + id: 'kubernetes.fetcher', + }); + +/** + * The interface for {@link kubernetesServiceLocatorExtensionPoint}. + * + * @public + */ +export interface KubernetesServiceLocatorExtensionPoint { + addServiceLocator(serviceLocator: KubernetesServiceLocator): void; +} + +/** + * An extension point the exposes the ability to configure a kubernetes service locator. + * + * @public + */ +export const kubernetesServiceLocatorExtensionPoint = + createExtensionPoint({ + id: 'kubernetes.service-locator', + }); diff --git a/plugins/kubernetes-node/src/index.ts b/plugins/kubernetes-node/src/index.ts index 09ea4123ce..162d5baf26 100644 --- a/plugins/kubernetes-node/src/index.ts +++ b/plugins/kubernetes-node/src/index.ts @@ -29,9 +29,5 @@ * @packageDocumentation */ -export { - kubernetesObjectsProviderExtensionPoint, - type KubernetesObjectsProviderExtensionPoint, -} from './extensions'; - +export * from './extensions'; export * from './types'; diff --git a/plugins/kubernetes-node/src/types/types.ts b/plugins/kubernetes-node/src/types/types.ts index 182f3ecfeb..a0f48d0c26 100644 --- a/plugins/kubernetes-node/src/types/types.ts +++ b/plugins/kubernetes-node/src/types/types.ts @@ -16,9 +16,12 @@ import { Entity } from '@backstage/catalog-model'; import { CustomResourceMatcher, + FetchResponse, + KubernetesFetchError, KubernetesRequestAuth, ObjectsByEntityResponse, } from '@backstage/plugin-kubernetes-common'; +import { JsonObject } from '@backstage/types'; /** * @@ -51,3 +54,200 @@ export interface KubernetesObjectsByEntity { export interface CustomResourcesByEntity extends KubernetesObjectsByEntity { customResources: CustomResourceMatcher[]; } + +/** + * Provider-specific authentication configuration + * @public + */ +export type AuthMetadata = Record; + +/** + * + * @public + */ +export interface ClusterDetails { + /** + * Specifies the name of the Kubernetes cluster. + */ + name: string; + url: string; + authMetadata: AuthMetadata; + skipTLSVerify?: boolean; + /** + * Whether to skip the lookup to the metrics server to retrieve pod resource usage. + * It is not guaranteed that the Kubernetes distro has the metrics server installed. + */ + skipMetricsLookup?: boolean; + caData?: string | undefined; + caFile?: string | undefined; + /** + * Specifies the link to the Kubernetes dashboard managing this cluster. + * @remarks + * Note that you should specify the app used for the dashboard + * using the dashboardApp property, in order to properly format + * links to kubernetes resources, otherwise it will assume that you're running the standard one. + * @see dashboardApp + * @see dashboardParameters + */ + dashboardUrl?: string; + /** + * Specifies the app that provides the Kubernetes dashboard. + * This will be used for formatting links to kubernetes objects inside the dashboard. + * @remarks + * The existing apps are: standard, rancher, openshift, gke, aks, eks + * Note that it will default to the regular dashboard provided by the Kubernetes project (standard). + * Note that you can add your own formatter by registering it to the clusterLinksFormatters dictionary. + * @defaultValue standard + * @see dashboardUrl + * @example + * ```ts + * import { clusterLinksFormatters } from '@backstage/plugin-kubernetes'; + * clusterLinksFormatters.myDashboard = (options) => ...; + * ``` + */ + dashboardApp?: string; + /** + * Specifies specific parameters used by some dashboard URL formatters. + * This is used by the GKE formatter which requires the project, region and cluster name. + * @see dashboardApp + */ + dashboardParameters?: JsonObject; + /** + * Specifies which custom resources to look for when returning an entity's + * Kubernetes resources. + */ + customResources?: CustomResourceMatcher[]; +} + +/** + * Used to load cluster details from different sources + * @public + */ +export interface KubernetesClustersSupplier { + /** + * Returns the cached list of clusters. + * + * Implementations _should_ cache the clusters and refresh them periodically, + * as getClusters is called whenever the list of clusters is needed. + */ + getClusters(): Promise; +} + +/** + * Authentication data used to make a request to Kubernetes + * @public + */ +export type KubernetesCredential = + | { type: 'bearer token'; token: string } + | { type: 'anonymous' }; + +/** + * + * @public + */ +export interface AuthenticationStrategy { + getCredential( + clusterDetails: ClusterDetails, + authConfig: KubernetesRequestAuth, + ): Promise; + 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 + */ +export interface ObjectToFetch { + objectType: KubernetesObjectTypes; // TODO - Review + group: string; + apiVersion: string; + plural: string; +} + +/** + * + * @public + */ +export interface CustomResource extends ObjectToFetch { + objectType: 'customresources'; +} + +/** + * + * @public + */ +export interface ObjectFetchParams { + serviceId: string; + clusterDetails: ClusterDetails; + credential: KubernetesCredential; + objectTypesToFetch: Set; + labelSelector?: string; + customResources: CustomResource[]; + namespace?: string; +} + +/** + * + * @public + */ +export interface FetchResponseWrapper { + errors: KubernetesFetchError[]; + responses: FetchResponse[]; +} + +/** + * Fetches information from a kubernetes cluster using the cluster details object to target a specific cluster + * + * @public + */ +export interface KubernetesFetcher { + fetchObjectsForService( + params: ObjectFetchParams, + ): Promise; + fetchPodMetricsByNamespaces( + clusterDetails: ClusterDetails, + credential: KubernetesCredential, + namespaces: Set, + labelSelector?: string, + ): Promise; +} +/** + * @public + */ +export interface ServiceLocatorRequestContext { + objectTypesToFetch: Set; + customResources: CustomResourceMatcher[]; +} + +/** + * Used to locate which cluster(s) a service is running on + * @public + */ +export interface KubernetesServiceLocator { + getClustersByEntity( + entity: Entity, + requestContext: ServiceLocatorRequestContext, + ): Promise<{ clusters: ClusterDetails[] }>; +} diff --git a/yarn.lock b/yarn.lock index 831a58a985..7a9560091f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7576,6 +7576,8 @@ __metadata: "@backstage/plugin-catalog-node": "workspace:^" "@backstage/plugin-kubernetes-common": "workspace:^" "@backstage/plugin-kubernetes-node": "workspace:^" + "@backstage/plugin-permission-backend": "workspace:^" + "@backstage/plugin-permission-backend-module-allow-all-policy": "workspace:^" "@backstage/plugin-permission-common": "workspace:^" "@backstage/plugin-permission-node": "workspace:^" "@backstage/types": "workspace:^" @@ -7675,6 +7677,7 @@ __metadata: "@backstage/catalog-model": "workspace:^" "@backstage/cli": "workspace:^" "@backstage/plugin-kubernetes-common": "workspace:^" + "@backstage/types": "workspace:^" languageName: unknown linkType: soft