diff --git a/.changeset/hot-items-smoke.md b/.changeset/hot-items-smoke.md index 52c5088d3e..c9b9c8a135 100644 --- a/.changeset/hot-items-smoke.md +++ b/.changeset/hot-items-smoke.md @@ -10,7 +10,7 @@ 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`: +For example, here's a simple example of a custom supplier in `packages/backend/src/plugins/kubernetes.ts`: ```diff -import { KubernetesBuilder } from '@backstage/plugin-kubernetes-backend'; @@ -21,9 +21,20 @@ For example, here's a simple example of this in `packages/backend/src/plugins/ku +} 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[] = []; ++ constructor(private clusterDetails: ClusterDetails[] = []) {} ++ ++ static create(refreshInterval: Duration) { ++ const clusterSupplier = new CustomClustersSupplier(); ++ // setup refresh, e.g. using a copy of https://github.com/backstage/backstage/blob/master/plugins/search-backend-node/src/runPeriodically.ts ++ runPeriodically( ++ () => clusterSupplier.refreshClusters(), ++ refreshInterval.toMillis(), ++ ); ++ return clusterSupplier; ++ } + + async refreshClusters(): Promise { + this.clusterDetails = []; // fetch from somewhere @@ -33,7 +44,7 @@ For example, here's a simple example of this in `packages/backend/src/plugins/ku + return this.clusterDetails; + } +} -+ + export default async function createPlugin( env: PluginEnvironment, ): Promise { @@ -43,14 +54,8 @@ For example, here's a simple example of this in `packages/backend/src/plugins/ku config: env.config, - }).build(); + }); -+ -+ const clusterSupplier = new CustomClustersSupplier(); -+ builder.setClusterSupplier(clusterSupplier); -+ ++ builder.setClusterSupplier( ++ CustomClustersSupplier.create(Duration.fromObject({ minutes: 60 })), ++ ); + const { router } = await builder.build(); - return router; - } ``` - -If you need to adjust the refresh interval from the default once per hour -you can call `builder.setClusterRefreshInterval`. diff --git a/docs/features/kubernetes/installation.md b/docs/features/kubernetes/installation.md index 4b4ba1139d..e920bd44ea 100644 --- a/docs/features/kubernetes/installation.md +++ b/docs/features/kubernetes/installation.md @@ -108,9 +108,20 @@ Change the following in `packages/backend/src/plugin/kubernetes.ts`: +} 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[] = []; ++ constructor(private clusterDetails: ClusterDetails[] = []) {} ++ ++ static create(refreshInterval: Duration) { ++ const clusterSupplier = new CustomClustersSupplier(); ++ // setup refresh, e.g. using a copy of https://github.com/backstage/backstage/blob/master/plugins/search-backend-node/src/runPeriodically.ts ++ runPeriodically( ++ () => clusterSupplier.refreshClusters(), ++ refreshInterval.toMillis(), ++ ); ++ return clusterSupplier; ++ } + + async refreshClusters(): Promise { + this.clusterDetails = []; // fetch from somewhere @@ -120,7 +131,7 @@ Change the following in `packages/backend/src/plugin/kubernetes.ts`: + return this.clusterDetails; + } +} -+ + export default async function createPlugin( env: PluginEnvironment, ): Promise { @@ -130,18 +141,12 @@ Change the following in `packages/backend/src/plugin/kubernetes.ts`: config: env.config, - }).build(); + }); -+ -+ const clusterSupplier = new CustomClustersSupplier(); -+ builder.setClusterSupplier(clusterSupplier); -+ ++ builder.setClusterSupplier( ++ CustomClustersSupplier.create(Duration.fromObject({ minutes: 60 })), ++ ); + const { router } = await builder.build(); - return router; - } ``` -If you need to adjust the refresh interval from the default once per hour -you can call `builder.setClusterRefreshInterval`. - ## 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 573198acf0..691b9443e5 100644 --- a/plugins/kubernetes-backend/api-report.md +++ b/plugins/kubernetes-backend/api-report.md @@ -4,6 +4,7 @@ ```ts import { Config } from '@backstage/config'; +import { Duration } from 'luxon'; import express from 'express'; import type { FetchResponse } from '@backstage/plugin-kubernetes-common'; import type { JsonObject } from '@backstage/types'; @@ -85,7 +86,9 @@ export class KubernetesBuilder { // (undocumented) build(): KubernetesBuilderReturn; // (undocumented) - protected buildClusterSupplier(): KubernetesClustersSupplier; + protected buildClusterSupplier( + refreshInterval: Duration, + ): KubernetesClustersSupplier; // (undocumented) protected buildCustomResources(): CustomResource[]; // (undocumented) @@ -125,10 +128,10 @@ export class KubernetesBuilder { // (undocumented) protected getServiceLocatorMethod(): ServiceLocatorMethod; // (undocumented) - setClusterRefreshInterval(refreshMs: number): this; - // (undocumented) setClusterSupplier(clusterSupplier?: KubernetesClustersSupplier): this; // (undocumented) + setDefaultClusterRefreshInterval(refreshInterval: Duration): this; + // (undocumented) setFetcher(fetcher?: KubernetesFetcher): this; // (undocumented) setObjectsProvider(objectsProvider?: KubernetesObjectsProvider): this; @@ -151,7 +154,6 @@ export type KubernetesBuilderReturn = Promise<{ // @public (undocumented) export interface KubernetesClustersSupplier { getClusters(): Promise; - refreshClusters(): Promise; } // Warning: (ae-missing-release-tag) "KubernetesEnvironment" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) diff --git a/plugins/kubernetes-backend/package.json b/plugins/kubernetes-backend/package.json index a55ec70780..f5eb974431 100644 --- a/plugins/kubernetes-backend/package.json +++ b/plugins/kubernetes-backend/package.json @@ -43,6 +43,7 @@ "@google-cloud/container": "^3.0.0", "@kubernetes/client-node": "^0.16.0", "@types/express": "^4.17.6", + "@types/luxon": "^2.0.4", "aws-sdk": "^2.840.0", "aws4": "^1.11.0", "compression": "^1.7.4", @@ -52,6 +53,7 @@ "fs-extra": "10.0.1", "helmet": "^5.0.2", "lodash": "^4.17.21", + "luxon": "^2.0.2", "morgan": "^1.10.0", "stream-buffers": "^3.0.2", "winston": "^3.2.1", @@ -60,8 +62,8 @@ "devDependencies": { "@backstage/cli": "^0.17.0-next.1", "@types/aws4": "^1.5.1", - "supertest": "^6.1.3", - "aws-sdk-mock": "^5.2.1" + "aws-sdk-mock": "^5.2.1", + "supertest": "^6.1.3" }, "files": [ "dist", diff --git a/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.ts b/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.ts index 893db0f5d0..1bde1226dd 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.ts @@ -77,8 +77,6 @@ export class ConfigClusterLocator implements KubernetesClustersSupplier { ); } - async refreshClusters(): Promise {} - async getClusters(): Promise { return this.clusterDetails; } diff --git a/plugins/kubernetes-backend/src/cluster-locator/GkeClusterLocator.test.ts b/plugins/kubernetes-backend/src/cluster-locator/GkeClusterLocator.test.ts index 51062066da..6bef8e0009 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/GkeClusterLocator.test.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/GkeClusterLocator.test.ts @@ -69,7 +69,6 @@ describe('GkeClusterLocator', () => { listClusters: mockedListClusters, } as any); - await sut.refreshClusters(); const result = await sut.getClusters(); expect(result).toStrictEqual([]); @@ -101,7 +100,6 @@ describe('GkeClusterLocator', () => { listClusters: mockedListClusters, } as any); - await sut.refreshClusters(); const result = await sut.getClusters(); expect(result).toStrictEqual([ @@ -139,7 +137,6 @@ describe('GkeClusterLocator', () => { listClusters: mockedListClusters, } as any); - await sut.refreshClusters(); const result = await sut.getClusters(); expect(result).toStrictEqual([ @@ -182,7 +179,6 @@ describe('GkeClusterLocator', () => { listClusters: mockedListClusters, } as any); - await sut.refreshClusters(); const result = await sut.getClusters(); expect(result).toStrictEqual([ @@ -221,7 +217,7 @@ describe('GkeClusterLocator', () => { listClusters: mockedListClusters, } as any); - await expect(sut.refreshClusters()).rejects.toThrow( + await expect(sut.getClusters()).rejects.toThrow( 'There was an error retrieving clusters from GKE for projectId=some-project region=some-region; caused by Error: some error', ); @@ -254,7 +250,6 @@ describe('GkeClusterLocator', () => { listClusters: mockedListClusters, } as any); - await sut.refreshClusters(); const result = await sut.getClusters(); expect(result).toStrictEqual([ diff --git a/plugins/kubernetes-backend/src/cluster-locator/GkeClusterLocator.ts b/plugins/kubernetes-backend/src/cluster-locator/GkeClusterLocator.ts index 8484f5006b..0eb25c2acd 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/GkeClusterLocator.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/GkeClusterLocator.ts @@ -17,6 +17,8 @@ import { Config } from '@backstage/config'; import { ForwardedError } from '@backstage/errors'; import * as container from '@google-cloud/container'; +import { Duration } from 'luxon'; +import { runPeriodically } from '../service/runPeriodically'; import { ClusterDetails, GKEClusterDetails, @@ -36,11 +38,13 @@ export class GkeClusterLocator implements KubernetesClustersSupplier { private readonly options: GkeClusterLocatorOptions, private readonly client: container.v1.ClusterManagerClient, private clusterDetails: GKEClusterDetails[] | undefined = undefined, + private hasClusterDetails: boolean = false, ) {} static fromConfigWithClient( config: Config, client: container.v1.ClusterManagerClient, + refreshInterval: Duration | undefined = undefined, ): GkeClusterLocator { const options = { projectId: config.getString('projectId'), @@ -50,17 +54,32 @@ export class GkeClusterLocator implements KubernetesClustersSupplier { config.getOptionalBoolean('skipMetricsLookup') ?? false, exposeDashboard: config.getOptionalBoolean('exposeDashboard') ?? false, }; - return new GkeClusterLocator(options, client); + const gkeClusterLocator = new GkeClusterLocator(options, client); + if (refreshInterval) { + runPeriodically( + () => gkeClusterLocator.refreshClusters(), + refreshInterval.toMillis(), + ); + } + return gkeClusterLocator; } - static fromConfig(config: Config): GkeClusterLocator { + static fromConfig( + config: Config, + refreshInterval: Duration | undefined = undefined, + ): GkeClusterLocator { return GkeClusterLocator.fromConfigWithClient( config, new container.v1.ClusterManagerClient(), + refreshInterval, ); } async getClusters(): Promise { + if (!this.hasClusterDetails) { + // refresh at least once when first called, when retries are disabled and in tests + await this.refreshClusters(); + } return this.clusterDetails ?? []; } @@ -97,6 +116,7 @@ export class GkeClusterLocator implements KubernetesClustersSupplier { } : {}), })); + this.hasClusterDetails = true; } catch (e) { throw new ForwardedError( `There was an error retrieving clusters from GKE for projectId=${projectId} region=${region}`, diff --git a/plugins/kubernetes-backend/src/cluster-locator/index.test.ts b/plugins/kubernetes-backend/src/cluster-locator/index.test.ts index 2bdc37c31c..cc7e7d07d6 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/index.test.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/index.test.ts @@ -46,7 +46,6 @@ describe('getCombinedClusterSupplier', () => { ); const clusterSupplier = getCombinedClusterSupplier(config); - await clusterSupplier.refreshClusters(); const result = await clusterSupplier.getClusters(); expect(result).toStrictEqual([ diff --git a/plugins/kubernetes-backend/src/cluster-locator/index.ts b/plugins/kubernetes-backend/src/cluster-locator/index.ts index fec92d06d7..53aeb44f8b 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/index.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/index.ts @@ -15,18 +15,16 @@ */ import { Config } from '@backstage/config'; +import { Duration } from 'luxon'; import { ClusterDetails, KubernetesClustersSupplier } from '../types/types'; import { ConfigClusterLocator } from './ConfigClusterLocator'; import { GkeClusterLocator } from './GkeClusterLocator'; class CombinedClustersSupplier implements KubernetesClustersSupplier { - constructor( - readonly clusterSuppliers: KubernetesClustersSupplier[], - private clusterDetails: ClusterDetails[] | undefined = undefined, - ) {} + constructor(readonly clusterSuppliers: KubernetesClustersSupplier[]) {} - async refreshClusters(): Promise { - this.clusterDetails = await Promise.all( + async getClusters(): Promise { + return await Promise.all( this.clusterSuppliers.map(supplier => supplier.getClusters()), ) .then(res => { @@ -36,14 +34,11 @@ class CombinedClustersSupplier implements KubernetesClustersSupplier { throw e; }); } - - async getClusters(): Promise { - return this.clusterDetails ?? []; - } } export const getCombinedClusterSupplier = ( rootConfig: Config, + refreshInterval: Duration | undefined = undefined, ): KubernetesClustersSupplier => { const clusterSuppliers = rootConfig .getConfigArray('kubernetes.clusterLocatorMethods') @@ -53,7 +48,10 @@ export const getCombinedClusterSupplier = ( case 'config': return ConfigClusterLocator.fromConfig(clusterLocatorMethod); case 'gke': - return GkeClusterLocator.fromConfig(clusterLocatorMethod); + return GkeClusterLocator.fromConfig( + clusterLocatorMethod, + refreshInterval, + ); default: throw new Error( `Unsupported kubernetes.clusterLocatorMethods: "${type}"`, diff --git a/plugins/kubernetes-backend/src/service-locator/MultiTenantServiceLocator.test.ts b/plugins/kubernetes-backend/src/service-locator/MultiTenantServiceLocator.test.ts index 974f554f45..0dce2305bf 100644 --- a/plugins/kubernetes-backend/src/service-locator/MultiTenantServiceLocator.test.ts +++ b/plugins/kubernetes-backend/src/service-locator/MultiTenantServiceLocator.test.ts @@ -20,7 +20,6 @@ import { MultiTenantServiceLocator } from './MultiTenantServiceLocator'; describe('MultiTenantConfigClusterLocator', () => { it('empty clusters returns empty cluster details', async () => { const sut = new MultiTenantServiceLocator({ - refreshClusters: async () => {}, getClusters: async () => [], }); @@ -31,7 +30,6 @@ describe('MultiTenantConfigClusterLocator', () => { it('one clusters returns one cluster details', async () => { const sut = new MultiTenantServiceLocator({ - refreshClusters: async () => {}, getClusters: async () => { return [ { @@ -58,7 +56,6 @@ describe('MultiTenantConfigClusterLocator', () => { it('two clusters returns two cluster details', async () => { const sut = new MultiTenantServiceLocator({ - refreshClusters: async () => {}, getClusters: async () => { return [ { diff --git a/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts b/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts index c3afa4fb6a..37dad8c3e4 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts @@ -59,7 +59,6 @@ describe('KubernetesBuilder', () => { }, ]; const clusterSupplier: KubernetesClustersSupplier = { - async refreshClusters() {}, async getClusters() { return clusters; }, @@ -180,7 +179,6 @@ describe('KubernetesBuilder', () => { }, ]; const clusterSupplier: KubernetesClustersSupplier = { - async refreshClusters() {}, async getClusters() { return clusters; }, diff --git a/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts b/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts index 9cdef9f00d..a3dfc8b005 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts @@ -17,6 +17,7 @@ import { Config } from '@backstage/config'; import express from 'express'; import Router from 'express-promise-router'; import { Logger } from 'winston'; +import { Duration } from 'luxon'; import { getCombinedClusterSupplier } from '../cluster-locator'; import { MultiTenantServiceLocator } from '../service-locator/MultiTenantServiceLocator'; import { @@ -36,7 +37,6 @@ import { KubernetesFanOutHandler, } from './KubernetesFanOutHandler'; import { KubernetesClientBasedFetcher } from './KubernetesFetcher'; -import { runPeriodically } from './runPeriodically'; export interface KubernetesEnvironment { logger: Logger; @@ -59,7 +59,9 @@ export type KubernetesBuilderReturn = Promise<{ export class KubernetesBuilder { private clusterSupplier?: KubernetesClustersSupplier; - private clusterRefreshMs: number = 60 * 60 * 1000; // defaults to once per hour + private defaultClusterRefreshInterval: Duration = Duration.fromObject({ + minutes: 60, + }); private objectsProvider?: KubernetesObjectsProvider; private fetcher?: KubernetesFetcher; private serviceLocator?: KubernetesServiceLocator; @@ -91,17 +93,9 @@ export class KubernetesBuilder { const fetcher = this.fetcher ?? this.buildFetcher(); - const clusterSupplier = this.clusterSupplier ?? this.buildClusterSupplier(); - - // we cannot use the regular scheduler here because all instances need this info - // and it is not persisted anywhere. - runPeriodically(async () => { - try { - await clusterSupplier.refreshClusters(); - } catch (e) { - logger.warn(`Failed to refresh kubernetes clusters: ${e}`); - } - }, this.clusterRefreshMs); + const clusterSupplier = + this.clusterSupplier ?? + this.buildClusterSupplier(this.defaultClusterRefreshInterval); const serviceLocator = this.serviceLocator ?? @@ -134,8 +128,8 @@ export class KubernetesBuilder { return this; } - public setClusterRefreshInterval(refreshMs: number) { - this.clusterRefreshMs = refreshMs; + public setDefaultClusterRefreshInterval(refreshInterval: Duration) { + this.defaultClusterRefreshInterval = refreshInterval; return this; } @@ -173,9 +167,11 @@ export class KubernetesBuilder { return customResources; } - protected buildClusterSupplier(): KubernetesClustersSupplier { + protected buildClusterSupplier( + refreshInterval: Duration, + ): KubernetesClustersSupplier { const config = this.env.config; - return getCombinedClusterSupplier(config); + return getCombinedClusterSupplier(config, refreshInterval); } protected buildObjectsProvider( diff --git a/plugins/kubernetes-backend/src/types/types.ts b/plugins/kubernetes-backend/src/types/types.ts index 512d916639..2d7e2d3da7 100644 --- a/plugins/kubernetes-backend/src/types/types.ts +++ b/plugins/kubernetes-backend/src/types/types.ts @@ -80,16 +80,11 @@ export type KubernetesObjectTypes = // Used to load cluster details from different sources export interface KubernetesClustersSupplier { - /** - * Refreshes the list of cluster from the source. - * - * This will be called periodically on a schedule to refresh the list - * of clusters. - */ - refreshClusters(): Promise; - /** * 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; }