diff --git a/.changeset/polite-knives-build.md b/.changeset/polite-knives-build.md new file mode 100644 index 0000000000..66ecbf0645 --- /dev/null +++ b/.changeset/polite-knives-build.md @@ -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' +``` diff --git a/docs/features/kubernetes/configuration.md b/docs/features/kubernetes/configuration.md index 07e3a8a828..494e51320a 100644 --- a/docs/features/kubernetes/configuration.md +++ b/docs/features/kubernetes/configuration.md @@ -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/ diff --git a/plugins/kubernetes-backend/api-report.md b/plugins/kubernetes-backend/api-report.md index 0ed5a31d9d..f79b14ede0 100644 --- a/plugins/kubernetes-backend/api-report.md +++ b/plugins/kubernetes-backend/api-report.md @@ -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 { diff --git a/plugins/kubernetes-backend/src/service-locator/SingleTenantServiceLocator.test.ts b/plugins/kubernetes-backend/src/service-locator/SingleTenantServiceLocator.test.ts new file mode 100644 index 0000000000..7e66ab96ea --- /dev/null +++ b/plugins/kubernetes-backend/src/service-locator/SingleTenantServiceLocator.test.ts @@ -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: [], + }); + }); +}); diff --git a/plugins/kubernetes-backend/src/service-locator/SingleTenantServiceLocator.ts b/plugins/kubernetes-backend/src/service-locator/SingleTenantServiceLocator.ts new file mode 100644 index 0000000000..e94d95776c --- /dev/null +++ b/plugins/kubernetes-backend/src/service-locator/SingleTenantServiceLocator.ts @@ -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 }; + }); + } +} diff --git a/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts b/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts index b05db36398..3bc7402179 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts @@ -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 { diff --git a/plugins/kubernetes-backend/src/types/types.ts b/plugins/kubernetes-backend/src/types/types.ts index 597984591f..6781dc1161 100644 --- a/plugins/kubernetes-backend/src/types/types.ts +++ b/plugins/kubernetes-backend/src/types/types.ts @@ -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