Move refresh handling into KubernetesClustersSupplier implementations

Signed-off-by: Luna Stadler <luc@spreadshirt.net>
This commit is contained in:
Luna Stadler
2022-04-08 16:30:54 +02:00
parent 8d050f714d
commit 2fc0e86616
13 changed files with 93 additions and 83 deletions
+18 -13
View File
@@ -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`.
+17 -12
View File
@@ -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
+6 -4
View File
@@ -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)
+4 -2
View File
@@ -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[]>;
}