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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user