Make Kubernetes cluster refresh more explicit
The `refreshClusters` method is now part of the `KubernetesClustersSupplier` interface definition and also documented. All existing cluster suppliers now implement it as well and the examples in the docs and in the CHANGELOG have been adjusted. Signed-off-by: Luna Stadler <luc@spreadshirt.net>
This commit is contained in:
@@ -21,12 +21,11 @@ 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[] = [];
|
||||
+
|
||||
+ async retrieveClusters() {
|
||||
+ async refreshClusters(): Promise<void> {
|
||||
+ this.clusterDetails = []; // fetch from somewhere
|
||||
+ }
|
||||
+
|
||||
@@ -34,7 +33,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> {
|
||||
@@ -46,18 +45,12 @@ For example, here's a simple example of this in `packages/backend/src/plugins/ku
|
||||
+ });
|
||||
+
|
||||
+ 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;
|
||||
}
|
||||
```
|
||||
|
||||
If you need to adjust the refresh interval from the default once per hour
|
||||
you can call `builder.setClusterRefreshInterval`.
|
||||
|
||||
@@ -108,12 +108,11 @@ 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[] = [];
|
||||
+
|
||||
+ async retrieveClusters() {
|
||||
+ async refreshClusters(): Promise<void> {
|
||||
+ this.clusterDetails = []; // fetch from somewhere
|
||||
+ }
|
||||
+
|
||||
@@ -121,7 +120,7 @@ Change the following in `packages/backend/src/plugin/kubernetes.ts`:
|
||||
+ return this.clusterDetails;
|
||||
+ }
|
||||
+}
|
||||
|
||||
+
|
||||
export default async function createPlugin(
|
||||
env: PluginEnvironment,
|
||||
): Promise<Router> {
|
||||
@@ -133,15 +132,6 @@ Change the following in `packages/backend/src/plugin/kubernetes.ts`:
|
||||
+ });
|
||||
+
|
||||
+ 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();
|
||||
@@ -149,6 +139,9 @@ Change the following in `packages/backend/src/plugin/kubernetes.ts`:
|
||||
}
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
@@ -125,6 +125,8 @@ export class KubernetesBuilder {
|
||||
// (undocumented)
|
||||
protected getServiceLocatorMethod(): ServiceLocatorMethod;
|
||||
// (undocumented)
|
||||
setClusterRefreshInterval(refreshMs: number): this;
|
||||
// (undocumented)
|
||||
setClusterSupplier(clusterSupplier?: KubernetesClustersSupplier): this;
|
||||
// (undocumented)
|
||||
setFetcher(fetcher?: KubernetesFetcher): this;
|
||||
@@ -148,8 +150,8 @@ export type KubernetesBuilderReturn = Promise<{
|
||||
//
|
||||
// @public (undocumented)
|
||||
export interface KubernetesClustersSupplier {
|
||||
// (undocumented)
|
||||
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)
|
||||
|
||||
@@ -77,6 +77,8 @@ export class ConfigClusterLocator implements KubernetesClustersSupplier {
|
||||
);
|
||||
}
|
||||
|
||||
async refreshClusters(): Promise<void> {}
|
||||
|
||||
async getClusters(): Promise<ClusterDetails[]> {
|
||||
return this.clusterDetails;
|
||||
}
|
||||
|
||||
@@ -69,6 +69,7 @@ describe('GkeClusterLocator', () => {
|
||||
listClusters: mockedListClusters,
|
||||
} as any);
|
||||
|
||||
await sut.refreshClusters();
|
||||
const result = await sut.getClusters();
|
||||
|
||||
expect(result).toStrictEqual([]);
|
||||
@@ -100,6 +101,7 @@ describe('GkeClusterLocator', () => {
|
||||
listClusters: mockedListClusters,
|
||||
} as any);
|
||||
|
||||
await sut.refreshClusters();
|
||||
const result = await sut.getClusters();
|
||||
|
||||
expect(result).toStrictEqual([
|
||||
@@ -137,6 +139,7 @@ describe('GkeClusterLocator', () => {
|
||||
listClusters: mockedListClusters,
|
||||
} as any);
|
||||
|
||||
await sut.refreshClusters();
|
||||
const result = await sut.getClusters();
|
||||
|
||||
expect(result).toStrictEqual([
|
||||
@@ -179,6 +182,7 @@ describe('GkeClusterLocator', () => {
|
||||
listClusters: mockedListClusters,
|
||||
} as any);
|
||||
|
||||
await sut.refreshClusters();
|
||||
const result = await sut.getClusters();
|
||||
|
||||
expect(result).toStrictEqual([
|
||||
@@ -217,7 +221,7 @@ describe('GkeClusterLocator', () => {
|
||||
listClusters: mockedListClusters,
|
||||
} as any);
|
||||
|
||||
await expect(sut.getClusters()).rejects.toThrow(
|
||||
await expect(sut.refreshClusters()).rejects.toThrow(
|
||||
'There was an error retrieving clusters from GKE for projectId=some-project region=some-region; caused by Error: some error',
|
||||
);
|
||||
|
||||
@@ -250,6 +254,7 @@ describe('GkeClusterLocator', () => {
|
||||
listClusters: mockedListClusters,
|
||||
} as any);
|
||||
|
||||
await sut.refreshClusters();
|
||||
const result = await sut.getClusters();
|
||||
|
||||
expect(result).toStrictEqual([
|
||||
|
||||
@@ -61,16 +61,11 @@ export class GkeClusterLocator implements KubernetesClustersSupplier {
|
||||
}
|
||||
|
||||
async getClusters(): Promise<ClusterDetails[]> {
|
||||
if (this.clusterDetails) {
|
||||
return this.clusterDetails;
|
||||
}
|
||||
|
||||
this.clusterDetails = await this.retrieveClusters();
|
||||
return this.clusterDetails;
|
||||
return this.clusterDetails ?? [];
|
||||
}
|
||||
|
||||
// TODO pass caData into the object
|
||||
async retrieveClusters(): Promise<GKEClusterDetails[]> {
|
||||
async refreshClusters(): Promise<void> {
|
||||
const {
|
||||
projectId,
|
||||
region,
|
||||
@@ -84,7 +79,7 @@ export class GkeClusterLocator implements KubernetesClustersSupplier {
|
||||
|
||||
try {
|
||||
const [response] = await this.client.listClusters(request);
|
||||
return (response.clusters ?? []).map(r => ({
|
||||
this.clusterDetails = (response.clusters ?? []).map(r => ({
|
||||
// TODO filter out clusters which don't have name or endpoint
|
||||
name: r.name ?? 'unknown',
|
||||
url: `https://${r.endpoint ?? ''}`,
|
||||
|
||||
@@ -15,9 +15,9 @@
|
||||
*/
|
||||
|
||||
import { Config, ConfigReader } from '@backstage/config';
|
||||
import { getCombinedClusterDetails } from './index';
|
||||
import { getCombinedClusterSupplier } from './index';
|
||||
|
||||
describe('getCombinedClusterDetails', () => {
|
||||
describe('getCombinedClusterSupplier', () => {
|
||||
it('should retrieve cluster details from config', async () => {
|
||||
const config: Config = new ConfigReader(
|
||||
{
|
||||
@@ -45,7 +45,9 @@ describe('getCombinedClusterDetails', () => {
|
||||
'ctx',
|
||||
);
|
||||
|
||||
const result = await getCombinedClusterDetails(config);
|
||||
const clusterSupplier = getCombinedClusterSupplier(config);
|
||||
await clusterSupplier.refreshClusters();
|
||||
const result = await clusterSupplier.getClusters();
|
||||
|
||||
expect(result).toStrictEqual([
|
||||
{
|
||||
@@ -99,7 +101,7 @@ describe('getCombinedClusterDetails', () => {
|
||||
'ctx',
|
||||
);
|
||||
|
||||
await expect(getCombinedClusterDetails(config)).rejects.toStrictEqual(
|
||||
expect(() => getCombinedClusterSupplier(config)).toThrowError(
|
||||
new Error('Unsupported kubernetes.clusterLocatorMethods: "magic"'),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -15,38 +15,51 @@
|
||||
*/
|
||||
|
||||
import { Config } from '@backstage/config';
|
||||
import { ClusterDetails } from '../types/types';
|
||||
import { ClusterDetails, KubernetesClustersSupplier } from '../types/types';
|
||||
import { ConfigClusterLocator } from './ConfigClusterLocator';
|
||||
import { GkeClusterLocator } from './GkeClusterLocator';
|
||||
|
||||
export const getCombinedClusterDetails = async (
|
||||
class CombinedClustersSupplier implements KubernetesClustersSupplier {
|
||||
constructor(
|
||||
readonly clusterSuppliers: KubernetesClustersSupplier[],
|
||||
private clusterDetails: ClusterDetails[] | undefined = undefined,
|
||||
) {}
|
||||
|
||||
async refreshClusters(): Promise<void> {
|
||||
this.clusterDetails = await Promise.all(
|
||||
this.clusterSuppliers.map(supplier => supplier.getClusters()),
|
||||
)
|
||||
.then(res => {
|
||||
return res.flat();
|
||||
})
|
||||
.catch(e => {
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
|
||||
async getClusters(): Promise<ClusterDetails[]> {
|
||||
return this.clusterDetails ?? [];
|
||||
}
|
||||
}
|
||||
|
||||
export const getCombinedClusterSupplier = (
|
||||
rootConfig: Config,
|
||||
): Promise<ClusterDetails[]> => {
|
||||
return Promise.all(
|
||||
rootConfig
|
||||
.getConfigArray('kubernetes.clusterLocatorMethods')
|
||||
.map(clusterLocatorMethod => {
|
||||
const type = clusterLocatorMethod.getString('type');
|
||||
switch (type) {
|
||||
case 'config':
|
||||
return ConfigClusterLocator.fromConfig(
|
||||
clusterLocatorMethod,
|
||||
).getClusters();
|
||||
case 'gke':
|
||||
return GkeClusterLocator.fromConfig(
|
||||
clusterLocatorMethod,
|
||||
).getClusters();
|
||||
default:
|
||||
throw new Error(
|
||||
`Unsupported kubernetes.clusterLocatorMethods: "${type}"`,
|
||||
);
|
||||
}
|
||||
}),
|
||||
)
|
||||
.then(res => {
|
||||
return res.flat();
|
||||
})
|
||||
.catch(e => {
|
||||
throw e;
|
||||
): KubernetesClustersSupplier => {
|
||||
const clusterSuppliers = rootConfig
|
||||
.getConfigArray('kubernetes.clusterLocatorMethods')
|
||||
.map(clusterLocatorMethod => {
|
||||
const type = clusterLocatorMethod.getString('type');
|
||||
switch (type) {
|
||||
case 'config':
|
||||
return ConfigClusterLocator.fromConfig(clusterLocatorMethod);
|
||||
case 'gke':
|
||||
return GkeClusterLocator.fromConfig(clusterLocatorMethod);
|
||||
default:
|
||||
throw new Error(
|
||||
`Unsupported kubernetes.clusterLocatorMethods: "${type}"`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return new CombinedClustersSupplier(clusterSuppliers);
|
||||
};
|
||||
|
||||
@@ -19,7 +19,10 @@ import { MultiTenantServiceLocator } from './MultiTenantServiceLocator';
|
||||
|
||||
describe('MultiTenantConfigClusterLocator', () => {
|
||||
it('empty clusters returns empty cluster details', async () => {
|
||||
const sut = new MultiTenantServiceLocator({ getClusters: async () => [] });
|
||||
const sut = new MultiTenantServiceLocator({
|
||||
refreshClusters: async () => {},
|
||||
getClusters: async () => [],
|
||||
});
|
||||
|
||||
const result = await sut.getClustersByServiceId('ignored');
|
||||
|
||||
@@ -28,6 +31,7 @@ describe('MultiTenantConfigClusterLocator', () => {
|
||||
|
||||
it('one clusters returns one cluster details', async () => {
|
||||
const sut = new MultiTenantServiceLocator({
|
||||
refreshClusters: async () => {},
|
||||
getClusters: async () => {
|
||||
return [
|
||||
{
|
||||
@@ -54,6 +58,7 @@ describe('MultiTenantConfigClusterLocator', () => {
|
||||
|
||||
it('two clusters returns two cluster details', async () => {
|
||||
const sut = new MultiTenantServiceLocator({
|
||||
refreshClusters: async () => {},
|
||||
getClusters: async () => {
|
||||
return [
|
||||
{
|
||||
|
||||
@@ -59,6 +59,7 @@ describe('KubernetesBuilder', () => {
|
||||
},
|
||||
];
|
||||
const clusterSupplier: KubernetesClustersSupplier = {
|
||||
async refreshClusters() {},
|
||||
async getClusters() {
|
||||
return clusters;
|
||||
},
|
||||
@@ -179,6 +180,7 @@ describe('KubernetesBuilder', () => {
|
||||
},
|
||||
];
|
||||
const clusterSupplier: KubernetesClustersSupplier = {
|
||||
async refreshClusters() {},
|
||||
async getClusters() {
|
||||
return clusters;
|
||||
},
|
||||
|
||||
@@ -17,7 +17,7 @@ import { Config } from '@backstage/config';
|
||||
import express from 'express';
|
||||
import Router from 'express-promise-router';
|
||||
import { Logger } from 'winston';
|
||||
import { getCombinedClusterDetails } from '../cluster-locator';
|
||||
import { getCombinedClusterSupplier } from '../cluster-locator';
|
||||
import { MultiTenantServiceLocator } from '../service-locator/MultiTenantServiceLocator';
|
||||
import {
|
||||
KubernetesObjectTypes,
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
KubernetesFanOutHandler,
|
||||
} from './KubernetesFanOutHandler';
|
||||
import { KubernetesClientBasedFetcher } from './KubernetesFetcher';
|
||||
import { runPeriodically } from './runPeriodically';
|
||||
|
||||
export interface KubernetesEnvironment {
|
||||
logger: Logger;
|
||||
@@ -58,6 +59,7 @@ export type KubernetesBuilderReturn = Promise<{
|
||||
|
||||
export class KubernetesBuilder {
|
||||
private clusterSupplier?: KubernetesClustersSupplier;
|
||||
private clusterRefreshMs: number = 60 * 60 * 1000; // defaults to once per hour
|
||||
private objectsProvider?: KubernetesObjectsProvider;
|
||||
private fetcher?: KubernetesFetcher;
|
||||
private serviceLocator?: KubernetesServiceLocator;
|
||||
@@ -91,6 +93,16 @@ export class KubernetesBuilder {
|
||||
|
||||
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 serviceLocator =
|
||||
this.serviceLocator ??
|
||||
this.buildServiceLocator(this.getServiceLocatorMethod(), clusterSupplier);
|
||||
@@ -122,6 +134,11 @@ export class KubernetesBuilder {
|
||||
return this;
|
||||
}
|
||||
|
||||
public setClusterRefreshInterval(refreshMs: number) {
|
||||
this.clusterRefreshMs = refreshMs;
|
||||
return this;
|
||||
}
|
||||
|
||||
public setObjectsProvider(objectsProvider?: KubernetesObjectsProvider) {
|
||||
this.objectsProvider = objectsProvider;
|
||||
return this;
|
||||
@@ -158,11 +175,7 @@ export class KubernetesBuilder {
|
||||
|
||||
protected buildClusterSupplier(): KubernetesClustersSupplier {
|
||||
const config = this.env.config;
|
||||
return {
|
||||
getClusters() {
|
||||
return getCombinedClusterDetails(config);
|
||||
},
|
||||
};
|
||||
return getCombinedClusterSupplier(config);
|
||||
}
|
||||
|
||||
protected buildObjectsProvider(
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Runs a function repeatedly, with a fixed wait between invocations.
|
||||
*
|
||||
* Supports async functions, and silently ignores exceptions and rejections.
|
||||
*
|
||||
* @param fn - The function to run. May return a Promise.
|
||||
* @param delayMs - The delay between a completed function invocation and the
|
||||
* next.
|
||||
* @returns A function that, when called, stops the invocation loop.
|
||||
*/
|
||||
export function runPeriodically(fn: () => any, delayMs: number): () => void {
|
||||
let cancel: () => void;
|
||||
let cancelled = false;
|
||||
const cancellationPromise = new Promise<void>(resolve => {
|
||||
cancel = () => {
|
||||
resolve();
|
||||
cancelled = true;
|
||||
};
|
||||
});
|
||||
|
||||
const startRefresh = async () => {
|
||||
while (!cancelled) {
|
||||
try {
|
||||
await fn();
|
||||
} catch {
|
||||
// ignore intentionally
|
||||
}
|
||||
|
||||
await Promise.race([
|
||||
new Promise(resolve => setTimeout(resolve, delayMs)),
|
||||
cancellationPromise,
|
||||
]);
|
||||
}
|
||||
};
|
||||
startRefresh();
|
||||
|
||||
return cancel!;
|
||||
}
|
||||
@@ -80,6 +80,17 @@ 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.
|
||||
*/
|
||||
getClusters(): Promise<ClusterDetails[]>;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user