Make Kubernetes clusters refreshable

To make it possible to refresh Kubernetes clusters responsible all
places that used ClusterDetails[] before now use the
KubernetesClustersSupplier directly and call getClusters() on it.  This
allows people to use implement custom KubernetesClustersSuppliers that
fetch the clusters from somewhere and refresh them using whatever
mechanism they might need.

Signed-off-by: Luna Stadler <luc@spreadshirt.net>
This commit is contained in:
Luna Stadler
2022-03-24 15:37:33 +01:00
parent e82483f20b
commit 3d45427666
7 changed files with 179 additions and 44 deletions
+63
View File
@@ -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<ClusterDetails[]> {
+ return this.clusterDetails;
+ }
+}
export default async function createPlugin(
env: PluginEnvironment,
): Promise<Router> {
- 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;
}
```
@@ -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
+59
View File
@@ -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<ClusterDetails[]> {
+ return this.clusterDetails;
+ }
+}
export default async function createPlugin(
env: PluginEnvironment,
): Promise<Router> {
- 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
+4 -5
View File
@@ -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;
@@ -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');
@@ -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<ClusterDetails[]> {
return this.clusterDetails;
return this.clusterSupplier.getClusters();
}
}
@@ -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,