[Kubernetes Backend Plugin] Added new CatalogRelationServiceLocator (#23509)

* Added new CatalogRelationServiceLocator

As described in #23491, in some environments it makes sense to have the kubernetes plugin show only referenced clusters rather than one or all

Signed-off-by: Severin Wischmann <severinwischmann@nianticlabs.com>

* Added Changeset

Signed-off-by: Severin Wischmann <severinwischmann@nianticlabs.com>

* Updated api-report

Signed-off-by: Severin Wischmann <severinwischmann@nianticlabs.com>

* Update the documentation to match the label

Signed-off-by: Severin Wischmann <severinwischmann@nianticlabs.com>

* Dropped underscore in variable name

Signed-off-by: Severin Wischmann <severinwischmann@nianticlabs.com>

* Moved filter to helper function

Signed-off-by: Severin Wischmann <severinwischmann@nianticlabs.com>

* Added more detail to the changeset

Signed-off-by: Severin Wischmann <severinwischmann@nianticlabs.com>

* Update .changeset/eleven-adults-add.md

Co-authored-by: Philipp Hugenroth <tudi2d@users.noreply.github.com>
Signed-off-by: swnia <119884634+swnia@users.noreply.github.com>

---------

Signed-off-by: Severin Wischmann <severinwischmann@nianticlabs.com>
Signed-off-by: swnia <119884634+swnia@users.noreply.github.com>
Co-authored-by: Philipp Hugenroth <tudi2d@users.noreply.github.com>
This commit is contained in:
swnia
2024-03-21 08:21:31 +01:00
committed by GitHub
parent ca3d320337
commit 9b1abac3cc
7 changed files with 295 additions and 30 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-kubernetes-backend': patch
---
Added a new service locator `CatalogRelationServiceLocator` that only returns clusters an entity lists in `relations.dependsOn`.
@@ -59,6 +59,8 @@ Valid values are:
- `singleTenant` - This configuration assumes that current component run on one cluster in provided clusters.
- `catalogRelation` - This configuration assumes that the current component runs only on all clusters it is dependant on.
### `clusterLocatorMethods`
This is an array used to determine where to retrieve cluster configuration from.
+9 -1
View File
@@ -163,6 +163,10 @@ export class KubernetesBuilder {
[key: string]: AuthenticationStrategy_2;
};
// (undocumented)
protected buildCatalogRelationServiceLocator(
clusterSupplier: KubernetesClustersSupplier_2,
): KubernetesServiceLocator_2;
// (undocumented)
protected buildClusterSupplier(
refreshInterval: Duration,
): KubernetesClustersSupplier_2;
@@ -397,7 +401,11 @@ export class ServiceAccountStrategy implements AuthenticationStrategy_2 {
}
// @public (undocumented)
export type ServiceLocatorMethod = 'multiTenant' | 'singleTenant' | 'http';
export type ServiceLocatorMethod =
| 'multiTenant'
| 'singleTenant'
| 'catalogRelation'
| 'http';
// @public @deprecated (undocumented)
export type ServiceLocatorRequestContext =
@@ -0,0 +1,169 @@
/*
* Copyright 2024 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.
*/
import { Entity } from '@backstage/catalog-model';
import { ServiceLocatorRequestContext } from '@backstage/plugin-kubernetes-node';
import { CatalogRelationServiceLocator } from './CatalogRelationServiceLocator';
describe('CatalogRelationServiceLocator', () => {
let serviceLocator: CatalogRelationServiceLocator;
beforeEach(() => {
// Mock the KubernetesClustersSupplier dependency
const clusterSupplier = {
getClusters: jest.fn().mockResolvedValue([
{
name: 'cluster1',
url: 'http://localhost:8080',
authMetadata: {},
},
{
name: 'cluster2',
url: 'http://localhost:8081',
authMetadata: {},
},
]),
};
serviceLocator = new CatalogRelationServiceLocator(clusterSupplier);
});
it('empty entity returns empty cluster details', async () => {
const result = await serviceLocator.getClustersByEntity(
{} as Entity,
{} as ServiceLocatorRequestContext,
);
expect(result).toStrictEqual({ clusters: [] });
});
it('empty clusters returns empty cluster details', async () => {
const crsl = new CatalogRelationServiceLocator({
getClusters: async () => [],
});
const testEntity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
relations: [
{ type: 'dependsOn', targetRef: 'resource:default/cluster1' },
{ type: 'dependsOn', targetRef: 'resource:default/cluster2' },
{ type: 'ownedBy', targetRef: 'group:default/group1' },
],
metadata: {
namespace: 'default',
name: 'testEntity',
},
};
const result = await crsl.getClustersByEntity(
testEntity,
{} as ServiceLocatorRequestContext,
);
expect(result).toStrictEqual({ clusters: [] });
});
it('should return all referenced clusters when entity depends on multiple clusters', async () => {
const testEntity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
relations: [
{ type: 'dependsOn', targetRef: 'resource:default/cluster1' },
{ type: 'dependsOn', targetRef: 'resource:default/cluster2' },
{ type: 'ownedBy', targetRef: 'group:default/group1' },
],
metadata: {
namespace: 'default',
name: 'testEntity',
},
};
const result = await serviceLocator.getClustersByEntity(
testEntity,
{} as ServiceLocatorRequestContext,
);
expect(result).toEqual({
clusters: [
{
name: 'cluster1',
url: 'http://localhost:8080',
authMetadata: {},
},
{
name: 'cluster2',
url: 'http://localhost:8081',
authMetadata: {},
},
],
});
});
it('should return one cluster when entity has one dependant cluster', async () => {
const testEntity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
relations: [
{ type: 'dependsOn', targetRef: 'resource:default/cluster1' },
{ type: 'ownedBy', targetRef: 'group:default/group1' },
],
metadata: {
namespace: 'default',
name: 'testEntity',
},
};
const result = await serviceLocator.getClustersByEntity(
testEntity,
{} as ServiceLocatorRequestContext,
);
expect(result).toEqual({
clusters: [
{
name: 'cluster1',
url: 'http://localhost:8080',
authMetadata: {},
},
],
});
});
it('should return an empty array when entity does not have dependsOn relation with resource target', async () => {
const testEntity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
relations: [
{ type: 'dependsOn', targetRef: 'resource:default/database1' },
{ type: 'dependsOn', targetRef: 'resource:default/database2' },
{ type: 'ownedBy', targetRef: 'group:default/group1' },
],
metadata: {
namespace: 'default',
name: 'testEntity',
},
};
const result = await serviceLocator.getClustersByEntity(
testEntity,
{} as ServiceLocatorRequestContext,
);
expect(result).toEqual({
clusters: [],
});
});
});
@@ -0,0 +1,66 @@
/*
* Copyright 2024 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.
*/
import { Entity } from '@backstage/catalog-model';
import {
ClusterDetails,
KubernetesClustersSupplier,
KubernetesServiceLocator,
ServiceLocatorRequestContext,
} from '@backstage/plugin-kubernetes-node';
// This locator assumes that service is located on the clusters it depends on
// Therefore it will return the clusters based on the relations it has or none otherwise
export class CatalogRelationServiceLocator implements KubernetesServiceLocator {
private readonly clusterSupplier: KubernetesClustersSupplier;
constructor(clusterSupplier: KubernetesClustersSupplier) {
this.clusterSupplier = clusterSupplier;
}
// As this implementation always returns all clusters serviceId is ignored here
getClustersByEntity(
entity: Entity,
_requestContext: ServiceLocatorRequestContext,
): Promise<{ clusters: ClusterDetails[] }> {
if (
entity.relations &&
entity.relations.some(
r => r.type === 'dependsOn' && r.targetRef.includes('resource:'),
)
) {
return this.clusterSupplier.getClusters().then(clusters => {
return {
clusters: clusters.filter(c =>
this.doesEntityDependOnCluster(entity, c),
),
};
});
}
return Promise.resolve({ clusters: [] });
}
protected doesEntityDependOnCluster(
entity: Entity,
cluster: ClusterDetails,
): boolean {
return entity.relations!.some(
rel =>
rel.type === 'dependsOn' &&
rel.targetRef ===
`resource:${entity.metadata.namespace ?? 'default'}/${cluster.name}`,
);
}
}
@@ -27,20 +27,38 @@ import Router from 'express-promise-router';
import { Duration } from 'luxon';
import { Logger } from 'winston';
import { getCombinedClusterSupplier } from '../cluster-locator';
import {
AnonymousStrategy,
DispatchStrategy,
GoogleStrategy,
ServiceAccountStrategy,
AwsIamStrategy,
GoogleServiceAccountStrategy,
AzureIdentityStrategy,
OidcStrategy,
AksStrategy,
AnonymousStrategy,
AwsIamStrategy,
AzureIdentityStrategy,
DispatchStrategy,
GoogleServiceAccountStrategy,
GoogleStrategy,
OidcStrategy,
ServiceAccountStrategy,
} from '../auth';
import { getCombinedClusterSupplier } from '../cluster-locator';
import { createLegacyAuthAdapters } from '@backstage/backend-common';
import {
AuthService,
BackstageCredentials,
DiscoveryService,
HttpAuthService,
} from '@backstage/backend-plugin-api';
import {
AuthMetadata,
AuthenticationStrategy,
CustomResource,
KubernetesClustersSupplier,
KubernetesFetcher,
KubernetesObjectTypes,
KubernetesObjectsProvider,
KubernetesServiceLocator,
} from '@backstage/plugin-kubernetes-node';
import { addResourceRoutesToRouter } from '../routes/resourcesRoutes';
import { CatalogRelationServiceLocator } from '../service-locator/CatalogRelationServiceLocator';
import { MultiTenantServiceLocator } from '../service-locator/MultiTenantServiceLocator';
import { SingleTenantServiceLocator } from '../service-locator/SingleTenantServiceLocator';
import {
@@ -48,29 +66,12 @@ import {
ObjectsByEntityRequest,
ServiceLocatorMethod,
} from '../types/types';
import {
AuthenticationStrategy,
AuthMetadata,
CustomResource,
KubernetesClustersSupplier,
KubernetesFetcher,
KubernetesObjectsProvider,
KubernetesObjectTypes,
KubernetesServiceLocator,
} from '@backstage/plugin-kubernetes-node';
import {
DEFAULT_OBJECTS,
KubernetesFanOutHandler,
} from './KubernetesFanOutHandler';
import { KubernetesClientBasedFetcher } from './KubernetesFetcher';
import { KubernetesProxy } from './KubernetesProxy';
import { createLegacyAuthAdapters } from '@backstage/backend-common';
import {
AuthService,
BackstageCredentials,
DiscoveryService,
HttpAuthService,
} from '@backstage/backend-plugin-api';
/**
*
@@ -306,6 +307,10 @@ export class KubernetesBuilder {
this.serviceLocator =
this.buildSingleTenantServiceLocator(clusterSupplier);
break;
case 'catalogRelation':
this.serviceLocator =
this.buildCatalogRelationServiceLocator(clusterSupplier);
break;
case 'http':
this.serviceLocator = this.buildHttpServiceLocator(clusterSupplier);
break;
@@ -330,6 +335,12 @@ export class KubernetesBuilder {
return new SingleTenantServiceLocator(clusterSupplier);
}
protected buildCatalogRelationServiceLocator(
clusterSupplier: KubernetesClustersSupplier,
): KubernetesServiceLocator {
return new CatalogRelationServiceLocator(clusterSupplier);
}
protected buildHttpServiceLocator(
_clusterSupplier: KubernetesClustersSupplier,
): KubernetesServiceLocator {
@@ -14,16 +14,20 @@
* limitations under the License.
*/
import { Logger } from 'winston';
import type { KubernetesRequestBody } from '@backstage/plugin-kubernetes-common';
import { Config } from '@backstage/config';
import type { KubernetesRequestBody } from '@backstage/plugin-kubernetes-common';
import * as k8sTypes from '@backstage/plugin-kubernetes-node';
import { Logger } from 'winston';
/**
*
* @public
*/
export type ServiceLocatorMethod = 'multiTenant' | 'singleTenant' | 'http'; // TODO implement http
export type ServiceLocatorMethod =
| 'multiTenant'
| 'singleTenant'
| 'catalogRelation'
| 'http'; // TODO implement http
/**
*