Move refresh handling into KubernetesClustersSupplier implementations
Signed-off-by: Luna Stadler <luc@spreadshirt.net>
This commit is contained in:
@@ -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<void> {
|
||||
+ 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<Router> {
|
||||
@@ -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`.
|
||||
|
||||
@@ -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<void> {
|
||||
+ 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<Router> {
|
||||
@@ -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
|
||||
|
||||
@@ -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<ClusterDetails[]>;
|
||||
refreshClusters(): Promise<void>;
|
||||
}
|
||||
|
||||
// Warning: (ae-missing-release-tag) "KubernetesEnvironment" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -77,8 +77,6 @@ export class ConfigClusterLocator implements KubernetesClustersSupplier {
|
||||
);
|
||||
}
|
||||
|
||||
async refreshClusters(): Promise<void> {}
|
||||
|
||||
async getClusters(): Promise<ClusterDetails[]> {
|
||||
return this.clusterDetails;
|
||||
}
|
||||
|
||||
@@ -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([
|
||||
|
||||
@@ -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<ClusterDetails[]> {
|
||||
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}`,
|
||||
|
||||
@@ -46,7 +46,6 @@ describe('getCombinedClusterSupplier', () => {
|
||||
);
|
||||
|
||||
const clusterSupplier = getCombinedClusterSupplier(config);
|
||||
await clusterSupplier.refreshClusters();
|
||||
const result = await clusterSupplier.getClusters();
|
||||
|
||||
expect(result).toStrictEqual([
|
||||
|
||||
@@ -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<void> {
|
||||
this.clusterDetails = await Promise.all(
|
||||
async getClusters(): Promise<ClusterDetails[]> {
|
||||
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<ClusterDetails[]> {
|
||||
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}"`,
|
||||
|
||||
@@ -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 [
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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<void>;
|
||||
|
||||
/**
|
||||
* 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<ClusterDetails[]>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user