diff --git a/.changeset/hot-items-smoke.md b/.changeset/hot-items-smoke.md new file mode 100644 index 0000000000..832604cd35 --- /dev/null +++ b/.changeset/hot-items-smoke.md @@ -0,0 +1,63 @@ +--- +'@backstage/plugin-kubernetes-backend': minor +--- + +**BREAKING** Custom cluster suppliers need to cache their getClusters result + +To allow custom `KubernetesClustersSupplier` instances to refresh the list of clusters +the `getClusters` method is now called whenever the list of clusters is needed. + +Existing `KubernetesClustersSupplier` implementations will need to ensure that `getClusters` +can be called frequently and should return a cached result from `getClusters` instead. + +For example, here's a simple example of this in `packages/backend/src/plugins/kubernetes.ts`: + +```diff +-import { KubernetesBuilder } from '@backstage/plugin-kubernetes-backend'; ++import { ++ ClusterDetails, ++ KubernetesBuilder, ++ KubernetesClustersSupplier, ++} from '@backstage/plugin-kubernetes-backend'; + import { Router } from 'express'; + import { PluginEnvironment } from '../types'; ++import { Duration } from 'luxon'; ++ ++export class CustomClustersSupplier implements KubernetesClustersSupplier { ++ private clusterDetails: ClusterDetails[] = []; ++ ++ async retrieveClusters() { ++ this.clusterDetails = []; // fetch from somewhere ++ } ++ ++ async getClusters(): Promise { ++ return this.clusterDetails; ++ } ++} + + export default async function createPlugin( + env: PluginEnvironment, + ): Promise { +- const { router } = await KubernetesBuilder.createBuilder({ ++ const builder = await KubernetesBuilder.createBuilder({ + logger: env.logger, + config: env.config, +- }).build(); ++ }); ++ ++ const clusterSupplier = new CustomClustersSupplier(); ++ env.scheduler ++ .createScheduledTaskRunner({ ++ frequency: Duration.fromObject({ minutes: 60 }), ++ timeout: Duration.fromObject({ minutes: 15 }), ++ }) ++ .run({ ++ id: 'refresh-kubernetes-clusters', ++ fn: clusterSupplier.retrieveClusters, ++ }); ++ builder.setClusterSupplier(clusterSupplier); ++ ++ const { router } = await builder.build(); + return router; + } +``` diff --git a/docs/features/kubernetes/configuration.md b/docs/features/kubernetes/configuration.md index 612f4903d9..d082d46df2 100644 --- a/docs/features/kubernetes/configuration.md +++ b/docs/features/kubernetes/configuration.md @@ -261,6 +261,12 @@ Kubernetes plugin. Defaults to `false`. +#### Custom `KubernetesClustersSupplier` + +If the configuration-based cluster locators do not work for your use-case, +it is also possible to implement a +[custom `KubernetesClustersSupplier`](installation.md#custom-cluster-discovery). + ### `customResources` (optional) Configures which [custom resources][3] to look for when returning an entity's diff --git a/docs/features/kubernetes/installation.md b/docs/features/kubernetes/installation.md index 2698580491..125889b858 100644 --- a/docs/features/kubernetes/installation.md +++ b/docs/features/kubernetes/installation.md @@ -90,6 +90,65 @@ async function main() { That's it! The Kubernetes frontend and backend have now been added to your Backstage app. +### Custom cluster discovery + +If either existing +[cluster locators](https://backstage.io/docs/features/kubernetes/configuration#clusterlocatormethods) +don't work for your use-case, it is possible to implement a custom +[KubernetesClustersSupplier](https://backstage.io/docs/reference/plugin-kubernetes-backend.kubernetesclusterssupplier). + +Change the following in `packages/backend/src/plugin/kubernetes.ts`: + +```diff +-import { KubernetesBuilder } from '@backstage/plugin-kubernetes-backend'; ++import { ++ ClusterDetails, ++ KubernetesBuilder, ++ KubernetesClustersSupplier, ++} from '@backstage/plugin-kubernetes-backend'; + import { Router } from 'express'; + import { PluginEnvironment } from '../types'; ++import { Duration } from 'luxon'; ++ ++export class CustomClustersSupplier implements KubernetesClustersSupplier { ++ private clusterDetails: ClusterDetails[] = []; ++ ++ async retrieveClusters() { ++ this.clusterDetails = []; // fetch from somewhere ++ } ++ ++ async getClusters(): Promise { ++ return this.clusterDetails; ++ } ++} + + export default async function createPlugin( + env: PluginEnvironment, + ): Promise { +- const { router } = await KubernetesBuilder.createBuilder({ ++ const builder = await KubernetesBuilder.createBuilder({ + logger: env.logger, + config: env.config, +- }).build(); ++ }); ++ ++ const clusterSupplier = new CustomClustersSupplier(); ++ env.scheduler ++ .createScheduledTaskRunner({ ++ frequency: Duration.fromObject({ minutes: 60 }), ++ timeout: Duration.fromObject({ minutes: 15 }), ++ }) ++ .run({ ++ id: 'refresh-kubernetes-clusters', ++ fn: clusterSupplier.retrieveClusters, ++ }); ++ builder.setClusterSupplier(clusterSupplier); ++ ++ const { router } = await builder.build(); + return router; + } +``` + ## Running Backstage locally Start the frontend and the backend app by diff --git a/plugins/kubernetes-backend/api-report.md b/plugins/kubernetes-backend/api-report.md index 8a3b64bd93..c00f68a13f 100644 --- a/plugins/kubernetes-backend/api-report.md +++ b/plugins/kubernetes-backend/api-report.md @@ -92,11 +92,11 @@ export class KubernetesBuilder { protected buildFetcher(): KubernetesFetcher; // (undocumented) protected buildHttpServiceLocator( - _clusterDetails: ClusterDetails[], + _clusterSupplier: KubernetesClustersSupplier, ): KubernetesServiceLocator; // (undocumented) protected buildMultiTenantServiceLocator( - clusterDetails: ClusterDetails[], + clusterSupplier: KubernetesClustersSupplier, ): KubernetesServiceLocator; // (undocumented) protected buildObjectsProvider( @@ -105,12 +105,12 @@ export class KubernetesBuilder { // (undocumented) protected buildRouter( objectsProvider: KubernetesObjectsProvider, - clusterDetails: ClusterDetails[], + clusterSupplier: KubernetesClustersSupplier, ): express.Router; // (undocumented) protected buildServiceLocator( method: ServiceLocatorMethod, - clusterDetails: ClusterDetails[], + clusterSupplier: KubernetesClustersSupplier, ): KubernetesServiceLocator; // (undocumented) static createBuilder(env: KubernetesEnvironment): KubernetesBuilder; @@ -137,7 +137,6 @@ export class KubernetesBuilder { // @public export type KubernetesBuilderReturn = Promise<{ router: express.Router; - clusterDetails: ClusterDetails[]; clusterSupplier: KubernetesClustersSupplier; customResources: CustomResource[]; fetcher: KubernetesFetcher; diff --git a/plugins/kubernetes-backend/src/service-locator/MultiTenantServiceLocator.test.ts b/plugins/kubernetes-backend/src/service-locator/MultiTenantServiceLocator.test.ts index 8586c5e445..f97161591f 100644 --- a/plugins/kubernetes-backend/src/service-locator/MultiTenantServiceLocator.test.ts +++ b/plugins/kubernetes-backend/src/service-locator/MultiTenantServiceLocator.test.ts @@ -19,7 +19,7 @@ import { MultiTenantServiceLocator } from './MultiTenantServiceLocator'; describe('MultiTenantConfigClusterLocator', () => { it('empty clusters returns empty cluster details', async () => { - const sut = new MultiTenantServiceLocator([]); + const sut = new MultiTenantServiceLocator({ getClusters: async () => [] }); const result = await sut.getClustersByServiceId('ignored'); @@ -27,14 +27,18 @@ describe('MultiTenantConfigClusterLocator', () => { }); it('one clusters returns one cluster details', async () => { - const sut = new MultiTenantServiceLocator([ - { - name: 'cluster1', - url: 'http://localhost:8080', - authProvider: 'serviceAccount', - serviceAccountToken: '12345', + const sut = new MultiTenantServiceLocator({ + getClusters: async () => { + return [ + { + name: 'cluster1', + url: 'http://localhost:8080', + authProvider: 'serviceAccount', + serviceAccountToken: '12345', + }, + ]; }, - ]); + }); const result = await sut.getClustersByServiceId('ignored'); @@ -49,19 +53,23 @@ describe('MultiTenantConfigClusterLocator', () => { }); it('two clusters returns two cluster details', async () => { - const sut = new MultiTenantServiceLocator([ - { - name: 'cluster1', - serviceAccountToken: 'token', - url: 'http://localhost:8080', - authProvider: 'serviceAccount', + const sut = new MultiTenantServiceLocator({ + getClusters: async () => { + return [ + { + name: 'cluster1', + serviceAccountToken: 'token', + url: 'http://localhost:8080', + authProvider: 'serviceAccount', + }, + { + name: 'cluster2', + url: 'http://localhost:8081', + authProvider: 'google', + }, + ]; }, - { - name: 'cluster2', - url: 'http://localhost:8081', - authProvider: 'google', - }, - ]); + }); const result = await sut.getClustersByServiceId('ignored'); diff --git a/plugins/kubernetes-backend/src/service-locator/MultiTenantServiceLocator.ts b/plugins/kubernetes-backend/src/service-locator/MultiTenantServiceLocator.ts index 9f874c7fc8..30c66a38d1 100644 --- a/plugins/kubernetes-backend/src/service-locator/MultiTenantServiceLocator.ts +++ b/plugins/kubernetes-backend/src/service-locator/MultiTenantServiceLocator.ts @@ -14,20 +14,24 @@ * limitations under the License. */ -import { ClusterDetails, KubernetesServiceLocator } from '../types/types'; +import { + ClusterDetails, + KubernetesClustersSupplier, + KubernetesServiceLocator, +} from '../types/types'; // This locator assumes that every service is located on every cluster // Therefore it will always return all clusters provided export class MultiTenantServiceLocator implements KubernetesServiceLocator { - private readonly clusterDetails: ClusterDetails[]; + private readonly clusterSupplier: KubernetesClustersSupplier; - constructor(clusterDetails: ClusterDetails[]) { - this.clusterDetails = clusterDetails; + constructor(clusterSupplier: KubernetesClustersSupplier) { + this.clusterSupplier = clusterSupplier; } // As this implementation always returns all clusters serviceId is ignored here // eslint-disable-next-line @typescript-eslint/no-unused-vars async getClustersByServiceId(_serviceId: string): Promise { - return this.clusterDetails; + return this.clusterSupplier.getClusters(); } } diff --git a/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts b/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts index a1eb60a239..6f5cfd0add 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts @@ -20,7 +20,6 @@ import { Logger } from 'winston'; import { getCombinedClusterDetails } from '../cluster-locator'; import { MultiTenantServiceLocator } from '../service-locator/MultiTenantServiceLocator'; import { - ClusterDetails, KubernetesObjectTypes, ServiceLocatorMethod, CustomResource, @@ -50,7 +49,6 @@ export interface KubernetesEnvironment { */ export type KubernetesBuilderReturn = Promise<{ router: express.Router; - clusterDetails: ClusterDetails[]; clusterSupplier: KubernetesClustersSupplier; customResources: CustomResource[]; fetcher: KubernetesFetcher; @@ -93,11 +91,9 @@ export class KubernetesBuilder { const clusterSupplier = this.clusterSupplier ?? this.buildClusterSupplier(); - const clusterDetails = await this.fetchClusterDetails(clusterSupplier); - const serviceLocator = this.serviceLocator ?? - this.buildServiceLocator(this.getServiceLocatorMethod(), clusterDetails); + this.buildServiceLocator(this.getServiceLocatorMethod(), clusterSupplier); const objectsProvider = this.objectsProvider ?? @@ -109,10 +105,9 @@ export class KubernetesBuilder { objectTypesToFetch: this.getObjectTypesToFetch(), }); - const router = this.buildRouter(objectsProvider, clusterDetails); + const router = this.buildRouter(objectsProvider, clusterSupplier); return { - clusterDetails, clusterSupplier, customResources, fetcher, @@ -185,13 +180,13 @@ export class KubernetesBuilder { protected buildServiceLocator( method: ServiceLocatorMethod, - clusterDetails: ClusterDetails[], + clusterSupplier: KubernetesClustersSupplier, ): KubernetesServiceLocator { switch (method) { case 'multiTenant': - return this.buildMultiTenantServiceLocator(clusterDetails); + return this.buildMultiTenantServiceLocator(clusterSupplier); case 'http': - return this.buildHttpServiceLocator(clusterDetails); + return this.buildHttpServiceLocator(clusterSupplier); default: throw new Error( `Unsupported kubernetes.clusterLocatorMethod "${method}"`, @@ -200,20 +195,20 @@ export class KubernetesBuilder { } protected buildMultiTenantServiceLocator( - clusterDetails: ClusterDetails[], + clusterSupplier: KubernetesClustersSupplier, ): KubernetesServiceLocator { - return new MultiTenantServiceLocator(clusterDetails); + return new MultiTenantServiceLocator(clusterSupplier); } protected buildHttpServiceLocator( - _clusterDetails: ClusterDetails[], + _clusterSupplier: KubernetesClustersSupplier, ): KubernetesServiceLocator { throw new Error('not implemented'); } protected buildRouter( objectsProvider: KubernetesObjectsProvider, - clusterDetails: ClusterDetails[], + clusterSupplier: KubernetesClustersSupplier, ): express.Router { const logger = this.env.logger; const router = Router(); @@ -236,6 +231,7 @@ export class KubernetesBuilder { }); router.get('/clusters', async (_, res) => { + const clusterDetails = await this.fetchClusterDetails(clusterSupplier); res.json({ items: clusterDetails.map(cd => ({ name: cd.name,