SingleTenant ServiceLocatorMethod for Kubernetes cluster by entity annotation (#20954)

* Filter Kubernetes cluster by entity annotation

Signed-off-by: Deepankumar Loganathan <deepan0433@gmail.com>

* Documentation updated

Signed-off-by: Deepankumar Loganathan <deepan0433@gmail.com>

* SingleTenantLocator implemented

Signed-off-by: Deepankumar Loganathan <deepan0433@gmail.com>

* API reports updated

Signed-off-by: Deepankumar Loganathan <deepan0433@gmail.com>

* spelling corrected

Signed-off-by: Deepankumar Loganathan <deepan0433@gmail.com>

* document updated and fixed review comments

Signed-off-by: Deepankumar Loganathan <deepan0433@gmail.com>

---------

Signed-off-by: Deepankumar Loganathan <deepan0433@gmail.com>
This commit is contained in:
Deepankumar
2023-11-15 20:54:22 +01:00
committed by GitHub
parent dc24f7ffdf
commit 52050ada6e
7 changed files with 260 additions and 3 deletions
+19
View File
@@ -0,0 +1,19 @@
---
'@backstage/plugin-kubernetes-backend': minor
---
You can now select `single` kubernetes cluster that the entity is part-of from all your defined kubernetes clusters, by passing `backstage.io/kubernetes-cluster` annotation with the defined cluster name.
If you do not specify the annotation by `default it fetches all` defined kubernetes cluster.
To apply
catalog-info.yaml
```diff
annotations:
'backstage.io/kubernetes-id': dice-roller
'backstage.io/kubernetes-namespace': dice-space
+ 'backstage.io/kubernetes-cluster': dice-cluster
'backstage.io/kubernetes-label-selector': 'app=my-app,component=front-end'
```
+19 -1
View File
@@ -51,11 +51,13 @@ kubernetes:
This configures how to determine which clusters a component is running in.
Currently, the only valid value is:
Valid values are:
- `multiTenant` - This configuration assumes that all components run on all the
provided clusters.
- `singleTenant` - This configuration assumes that current component run on one cluster in provided clusters.
### `clusterLocatorMethods`
This is an array used to determine where to retrieve cluster configuration from.
@@ -577,6 +579,22 @@ for more info.
'backstage.io/kubernetes-label-selector': 'app=my-app,component=front-end'
```
### Cluster Selection annotation
This is applicable only for `singleTenant` serviceLocatorMethod.
You can now select `single` kubernetes cluster that the entity is part-of from all your defined kubernetes clusters. To apply this use the following annotation.
SingleTenant Cluster:
```yaml
'backstage.io/kubernetes-cluster': dice-cluster
```
In the example above, we configured the "backstage.io/kubernetes-cluster" annotation on the entity `catalog-info.yaml` file to specify that the current component is running in a single cluster called "dice-cluster", so this cluster must have been specified in the `app-config.yaml`, under the Kubernetes clusters configuration (for more details, see [`Configuring Kubernetes clusters`](#configuring-kubernetes-clusters)).
If you do not specify the annotation by `default Backstage fetches all` defined Kubernetes cluster.
[1]: https://cloud.google.com/kubernetes-engine
[2]: https://cloud.google.com/docs/authentication/production#linux-or-macos
[3]: https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/
+5 -1
View File
@@ -210,6 +210,10 @@ export class KubernetesBuilder {
clusterSupplier: KubernetesClustersSupplier,
): KubernetesServiceLocator;
// (undocumented)
protected buildSingleTenantServiceLocator(
clusterSupplier: KubernetesClustersSupplier,
): KubernetesServiceLocator;
// (undocumented)
static createBuilder(env: KubernetesEnvironment): KubernetesBuilder;
// (undocumented)
protected readonly env: KubernetesEnvironment;
@@ -452,7 +456,7 @@ export class ServiceAccountStrategy implements AuthenticationStrategy {
}
// @public (undocumented)
export type ServiceLocatorMethod = 'multiTenant' | 'http';
export type ServiceLocatorMethod = 'multiTenant' | 'singleTenant' | 'http';
// @public (undocumented)
export interface ServiceLocatorRequestContext {
@@ -0,0 +1,151 @@
/*
* Copyright 2023 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 '@backstage/backend-common';
import { Entity } from '@backstage/catalog-model';
import { ServiceLocatorRequestContext } from '../types/types';
import { SingleTenantServiceLocator } from './SingleTenantServiceLocator';
describe('SingleTenantConfigClusterLocator', () => {
it('empty clusters returns empty cluster details', async () => {
const sut = new SingleTenantServiceLocator({
getClusters: async () => [],
});
const result = await sut.getClustersByEntity(
{} as Entity,
{} as ServiceLocatorRequestContext,
);
expect(result).toStrictEqual({ clusters: [] });
});
it('one cluster return from two clusters', async () => {
const sut = new SingleTenantServiceLocator({
getClusters: async () => {
return [
{
name: 'cluster1',
url: 'http://localhost:8080',
authMetadata: {},
},
{
name: 'cluster2',
url: 'http://localhost:8081',
authMetadata: {},
},
];
},
});
const testEntity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
namespace: 'default',
name: 'testEntity',
annotations: {
'backstage.io/kubernetes-cluster': 'cluster1',
},
},
};
const result = await sut.getClustersByEntity(
testEntity as Entity,
{} as ServiceLocatorRequestContext,
);
expect(result).toStrictEqual({
clusters: [
{
name: 'cluster1',
url: 'http://localhost:8080',
authMetadata: {},
},
],
});
});
it('no annotation return all cluster', async () => {
const definedClusters = [
{
name: 'cluster1',
url: 'http://localhost:8080',
authMetadata: {},
},
{
name: 'cluster2',
url: 'http://localhost:8081',
authMetadata: {},
},
];
const sut = new SingleTenantServiceLocator({
getClusters: async () => {
return definedClusters;
},
});
const result = await sut.getClustersByEntity(
{} as Entity,
{} as ServiceLocatorRequestContext,
);
expect(result).toStrictEqual({
clusters: definedClusters,
});
});
it('wrong annotation returns empty cluster', async () => {
const sut = new SingleTenantServiceLocator({
getClusters: async () => {
return [
{
name: 'cluster1',
url: 'http://localhost:8080',
authMetadata: {},
},
{
name: 'cluster2',
url: 'http://localhost:8081',
authMetadata: {},
},
];
},
});
const testEntity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
namespace: 'default',
name: 'testEntity',
annotations: {
'backstage.io/kubernetes-cluster': 'cluster3',
},
},
};
const result = await sut.getClustersByEntity(
testEntity as Entity,
{} as ServiceLocatorRequestContext,
);
expect(result).toStrictEqual({
clusters: [],
});
});
});
@@ -0,0 +1,54 @@
/*
* Copyright 2023 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 '../types/types';
// This locator assumes that service is located on one cluster
// Therefore it will always return specified cluster provided in backstage.io/kubernetes-cluster annotation
// If backstage.io/kubernetes-cluster annotation not provided will always return all cluster provided
export class SingleTenantServiceLocator 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[] }> {
return this.clusterSupplier.getClusters().then(clusters => {
if (_entity.metadata?.annotations?.['backstage.io/kubernetes-cluster']) {
return {
clusters: clusters.filter(
c =>
c.name ===
_entity.metadata?.annotations?.[
'backstage.io/kubernetes-cluster'
],
),
};
}
return { clusters };
});
}
}
@@ -43,6 +43,7 @@ import {
import { addResourceRoutesToRouter } from '../routes/resourcesRoutes';
import { MultiTenantServiceLocator } from '../service-locator/MultiTenantServiceLocator';
import { SingleTenantServiceLocator } from '../service-locator/SingleTenantServiceLocator';
import {
CustomResource,
KubernetesClustersSupplier,
@@ -276,6 +277,10 @@ export class KubernetesBuilder {
this.serviceLocator =
this.buildMultiTenantServiceLocator(clusterSupplier);
break;
case 'singleTenant':
this.serviceLocator =
this.buildSingleTenantServiceLocator(clusterSupplier);
break;
case 'http':
this.serviceLocator = this.buildHttpServiceLocator(clusterSupplier);
break;
@@ -294,6 +299,12 @@ export class KubernetesBuilder {
return new MultiTenantServiceLocator(clusterSupplier);
}
protected buildSingleTenantServiceLocator(
clusterSupplier: KubernetesClustersSupplier,
): KubernetesServiceLocator {
return new SingleTenantServiceLocator(clusterSupplier);
}
protected buildHttpServiceLocator(
_clusterSupplier: KubernetesClustersSupplier,
): KubernetesServiceLocator {
@@ -144,7 +144,7 @@ export interface KubernetesServiceLocator {
*
* @public
*/
export type ServiceLocatorMethod = 'multiTenant' | 'http'; // TODO implement http
export type ServiceLocatorMethod = 'multiTenant' | 'singleTenant' | 'http'; // TODO implement http
/**
* Provider-specific authentication configuration