diff --git a/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts b/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts index 6294b3a7ed..8c562b5200 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts @@ -57,7 +57,7 @@ describe('KubernetesClientProvider', () => { clientMock.listServiceForAllNamespaces.mockRejectedValue(errorResponse); - const result = await sut.fetchObjectsByServiceId( + const result = await sut.fetchObjectsForService( 'some-service', { name: 'cluster1', @@ -120,7 +120,7 @@ describe('KubernetesClientProvider', () => { }, }); - const result = await sut.fetchObjectsByServiceId( + const result = await sut.fetchObjectsForService( 'some-service', { name: 'cluster1', @@ -169,7 +169,7 @@ describe('KubernetesClientProvider', () => { }); it('should throw error on unknown type', () => { expect(() => - sut.fetchObjectsByServiceId( + sut.fetchObjectsForService( 'some-service', { name: 'cluster1', diff --git a/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts b/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts index f66a45816a..3bfb651d52 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts @@ -105,15 +105,19 @@ export class KubernetesClientBasedFetcher implements KubernetesFetcher { this.logger = logger; } - fetchObjectsByServiceId( + fetchObjectsForService( serviceId: string, clusterDetails: ClusterDetails, objectTypesToFetch: Set, + labelSelector: string, ): Promise { const fetchResults = Array.from(objectTypesToFetch).map(type => { - return this.fetchByObjectType(serviceId, clusterDetails, type).catch( - captureKubernetesErrorsRethrowOthers, - ); + return this.fetchByObjectType( + serviceId, + clusterDetails, + type, + labelSelector, + ).catch(captureKubernetesErrorsRethrowOthers); }); return Promise.all(fetchResults).then(fetchResultsToResponseWrapper); @@ -124,42 +128,53 @@ export class KubernetesClientBasedFetcher implements KubernetesFetcher { serviceId: string, clusterDetails: ClusterDetails, type: KubernetesObjectTypes, + labelSelector: string, ): Promise { switch (type) { case 'pods': - return this.fetchPodsByServiceId(serviceId, clusterDetails).then(r => ({ + return this.fetchPodsForService( + serviceId, + clusterDetails, + labelSelector, + ).then(r => ({ type: type, resources: r, })); case 'configmaps': - return this.fetchConfigMapsByServiceId( + return this.fetchConfigMapsForService( serviceId, clusterDetails, + labelSelector, ).then(r => ({ type: type, resources: r })); case 'deployments': - return this.fetchDeploymentsByServiceId( + return this.fetchDeploymentsForService( serviceId, clusterDetails, + labelSelector, ).then(r => ({ type: type, resources: r })); case 'replicasets': - return this.fetchReplicaSetsByServiceId( + return this.fetchReplicaSetsForService( serviceId, clusterDetails, + labelSelector, ).then(r => ({ type: type, resources: r })); case 'services': - return this.fetchServicesByServiceId( + return this.fetchServicesForService( serviceId, clusterDetails, + labelSelector, ).then(r => ({ type: type, resources: r })); case 'horizontalpodautoscalers': - return this.fetchHorizontalPodAutoscalersByServiceId( + return this.fetchHorizontalPodAutoscalersForService( serviceId, clusterDetails, + labelSelector, ).then(r => ({ type: type, resources: r })); case 'ingresses': - return this.fetchIngressesByServiceId( + return this.fetchIngressesForService( serviceId, clusterDetails, + labelSelector, ).then(r => ({ type: type, resources: r })); default: // unrecognised type @@ -192,79 +207,60 @@ export class KubernetesClientBasedFetcher implements KubernetesFetcher { }); } - private fetchServicesByServiceId( + private fetchServicesForService( serviceId: string, clusterDetails: ClusterDetails, + labelSelector: string, ): Promise> { return this.singleClusterFetch(clusterDetails, ({ core }) => - core.listServiceForAllNamespaces( - false, - '', - '', - `backstage.io/kubernetes-id=${serviceId}`, - ), + core.listServiceForAllNamespaces(false, '', '', labelSelector), ); } - private fetchPodsByServiceId( + private fetchPodsForService( serviceId: string, clusterDetails: ClusterDetails, + labelSelector: string, ): Promise> { return this.singleClusterFetch(clusterDetails, ({ core }) => - core.listPodForAllNamespaces( - false, - '', - '', - `backstage.io/kubernetes-id=${serviceId}`, - ), + core.listPodForAllNamespaces(false, '', '', labelSelector), ); } - private fetchConfigMapsByServiceId( + private fetchConfigMapsForService( serviceId: string, clusterDetails: ClusterDetails, + labelSelector: string, ): Promise> { return this.singleClusterFetch(clusterDetails, ({ core }) => - core.listConfigMapForAllNamespaces( - false, - '', - '', - `backstage.io/kubernetes-id=${serviceId}`, - ), + core.listConfigMapForAllNamespaces(false, '', '', labelSelector), ); } - private fetchDeploymentsByServiceId( + private fetchDeploymentsForService( serviceId: string, clusterDetails: ClusterDetails, + labelSelector: string, ): Promise> { return this.singleClusterFetch(clusterDetails, ({ apps }) => - apps.listDeploymentForAllNamespaces( - false, - '', - '', - `backstage.io/kubernetes-id=${serviceId}`, - ), + apps.listDeploymentForAllNamespaces(false, '', '', labelSelector), ); } - private fetchReplicaSetsByServiceId( + private fetchReplicaSetsForService( serviceId: string, clusterDetails: ClusterDetails, + labelSelector: string, ): Promise> { return this.singleClusterFetch(clusterDetails, ({ apps }) => - apps.listReplicaSetForAllNamespaces( - false, - '', - '', - `backstage.io/kubernetes-id=${serviceId}`, - ), + apps.listReplicaSetForAllNamespaces(false, '', '', labelSelector), ); } - private fetchHorizontalPodAutoscalersByServiceId( + private fetchHorizontalPodAutoscalersForService( serviceId: string, clusterDetails: ClusterDetails, + labelSelector: string, ): Promise> { return this.singleClusterFetch( clusterDetails, @@ -273,14 +269,15 @@ export class KubernetesClientBasedFetcher implements KubernetesFetcher { false, '', '', - `backstage.io/kubernetes-id=${serviceId}`, + labelSelector, ), ); } - private fetchIngressesByServiceId( + private fetchIngressesForService( serviceId: string, clusterDetails: ClusterDetails, + labelSelector: string, ): Promise> { return this.singleClusterFetch( clusterDetails, @@ -289,7 +286,7 @@ export class KubernetesClientBasedFetcher implements KubernetesFetcher { false, '', '', - `backstage.io/kubernetes-id=${serviceId}`, + labelSelector, ), ); } diff --git a/plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.test.ts b/plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.test.ts index 8c26ded2d4..f413d080c6 100644 --- a/plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.test.ts +++ b/plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { handleGetKubernetesObjectsByServiceId } from './getKubernetesObjectsByServiceIdHandler'; +import { handleGetKubernetesObjectsForService } from './getKubernetesObjectsForServiceHandler'; import { getVoidLogger } from '@backstage/backend-common'; import { ClusterDetails } from '..'; @@ -81,10 +81,10 @@ describe('handleGetKubernetesObjectsByServiceId', () => { mockFetch(fetchObjectsByServiceId); - const result = await handleGetKubernetesObjectsByServiceId( + const result = await handleGetKubernetesObjectsForService( TEST_SERVICE_ID, { - fetchObjectsByServiceId, + fetchObjectsForService: fetchObjectsByServiceId, }, { getClustersByServiceId, @@ -155,10 +155,10 @@ describe('handleGetKubernetesObjectsByServiceId', () => { mockFetch(fetchObjectsByServiceId); - const result = await handleGetKubernetesObjectsByServiceId( + const result = await handleGetKubernetesObjectsForService( TEST_SERVICE_ID, { - fetchObjectsByServiceId, + fetchObjectsForService: fetchObjectsByServiceId, }, { getClustersByServiceId, diff --git a/plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.ts b/plugins/kubernetes-backend/src/service/getKubernetesObjectsForServiceHandler.ts similarity index 90% rename from plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.ts rename to plugins/kubernetes-backend/src/service/getKubernetesObjectsForServiceHandler.ts index 6fc6092bd6..5f31248790 100644 --- a/plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.ts +++ b/plugins/kubernetes-backend/src/service/getKubernetesObjectsForServiceHandler.ts @@ -26,12 +26,13 @@ import { import { KubernetesAuthTranslator } from '../kubernetes-auth-translator/types'; import { KubernetesAuthTranslatorGenerator } from '../kubernetes-auth-translator/KubernetesAuthTranslatorGenerator'; -export type GetKubernetesObjectsByServiceIdHandler = ( +export type GetKubernetesObjectsForServiceHandler = ( serviceId: string, fetcher: KubernetesFetcher, serviceLocator: KubernetesServiceLocator, logger: Logger, requestBody: AuthRequestBody, + labelSelector: string, objectsToFetch?: Set, ) => Promise; @@ -46,12 +47,13 @@ const DEFAULT_OBJECTS = new Set([ ]); // Fans out the request to all clusters that the service lives in, aggregates their responses together -export const handleGetKubernetesObjectsByServiceId: GetKubernetesObjectsByServiceIdHandler = async ( +export const handleGetKubernetesObjectsForService: GetKubernetesObjectsForServiceHandler = async ( serviceId, fetcher, serviceLocator, logger, requestBody, + labelSelector: string, objectsToFetch = DEFAULT_OBJECTS, ) => { const clusterDetails: ClusterDetails[] = await serviceLocator.getClustersByServiceId( @@ -81,7 +83,7 @@ export const handleGetKubernetesObjectsByServiceId: GetKubernetesObjectsByServic return Promise.all( clusterDetailsDecoratedForAuth.map(cd => { return fetcher - .fetchObjectsByServiceId(serviceId, cd, objectsToFetch) + .fetchObjectsForService(serviceId, cd, objectsToFetch, labelSelector) .then(result => { return { cluster: { diff --git a/plugins/kubernetes-backend/src/service/router.test.ts b/plugins/kubernetes-backend/src/service/router.test.ts index 6f9fbbf70f..b63b63ead6 100644 --- a/plugins/kubernetes-backend/src/service/router.test.ts +++ b/plugins/kubernetes-backend/src/service/router.test.ts @@ -32,7 +32,7 @@ describe('router', () => { beforeAll(async () => { kubernetesFetcher = { - fetchObjectsByServiceId: jest.fn(), + fetchObjectsForService: jest.fn(), }; kubernetesServiceLocator = { diff --git a/plugins/kubernetes-backend/src/service/router.ts b/plugins/kubernetes-backend/src/service/router.ts index b73115e1e4..5462b5c9fb 100644 --- a/plugins/kubernetes-backend/src/service/router.ts +++ b/plugins/kubernetes-backend/src/service/router.ts @@ -22,9 +22,9 @@ import { MultiTenantServiceLocator } from '../service-locator/MultiTenantService import { KubernetesClientBasedFetcher } from './KubernetesFetcher'; import { KubernetesClientProvider } from './KubernetesClientProvider'; import { - GetKubernetesObjectsByServiceIdHandler, - handleGetKubernetesObjectsByServiceId, -} from './getKubernetesObjectsByServiceIdHandler'; + GetKubernetesObjectsForServiceHandler, + handleGetKubernetesObjectsForService, +} from './getKubernetesObjectsForServiceHandler'; import { AuthRequestBody, KubernetesServiceLocator, @@ -64,13 +64,14 @@ export const makeRouter = ( logger: Logger, fetcher: KubernetesFetcher, serviceLocator: KubernetesServiceLocator, - handleGetByServiceId: GetKubernetesObjectsByServiceIdHandler, + handleGetByServiceId: GetKubernetesObjectsForServiceHandler, ): express.Router => { const router = Router(); router.use(express.json()); router.post('/services/:serviceId', async (req, res) => { const serviceId = req.params.serviceId; + const labelSelector = req.query.labelSelector; const requestBody: AuthRequestBody = req.body; try { const response = await handleGetByServiceId( @@ -79,6 +80,7 @@ export const makeRouter = ( serviceLocator, logger, requestBody, + labelSelector, ); res.send(response); } catch (e) { @@ -119,6 +121,6 @@ export async function createRouter( logger, fetcher, serviceLocator, - handleGetKubernetesObjectsByServiceId, + handleGetKubernetesObjectsForService, ); } diff --git a/plugins/kubernetes-backend/src/types/types.ts b/plugins/kubernetes-backend/src/types/types.ts index 1422511c21..0425687f48 100644 --- a/plugins/kubernetes-backend/src/types/types.ts +++ b/plugins/kubernetes-backend/src/types/types.ts @@ -110,10 +110,11 @@ export interface IngressesFetchResponse { // Fetches information from a kubernetes cluster using the cluster details object // to target a specific cluster export interface KubernetesFetcher { - fetchObjectsByServiceId( + fetchObjectsForService( serviceId: string, clusterDetails: ClusterDetails, objectTypesToFetch: Set, + labelSelector: string, ): Promise; } diff --git a/plugins/kubernetes/README.md b/plugins/kubernetes/README.md index 1ad478bd89..e6f1f2355a 100644 --- a/plugins/kubernetes/README.md +++ b/plugins/kubernetes/README.md @@ -14,7 +14,12 @@ It is only meant for local development, and the setup for it can be found inside ## Surfacing your Kubernetes components as part of an entity -### Adding the entity annotation +There are 2 ways to surface your kubernetes components as part of an entity. +The label selector takes precedence over the annotation/service id. + +### Common `backstage.io/kubernetes-id` label + +#### Adding the entity annotation In order for Backstage to detect that an entity has Kubernetes components, the following annotation should be added to the entity. @@ -24,7 +29,7 @@ annotations: 'backstage.io/kubernetes-id': dice-roller ``` -### Labeling Kubernetes components +#### Labeling Kubernetes components In order for Kubernetes components to show up in the service catalog as a part of an entity, Kubernetes components must be labeled with the following label: @@ -32,3 +37,19 @@ as a part of an entity, Kubernetes components must be labeled with the following ```yaml 'backstage.io/kubernetes-id': ``` + +### Full label selector + +#### Adding the entity label selector to the spec + +In order for Backstage to detect that an entity has kubernetes components the `kubernetes.selector` must have a valid selector. (Currently only matchLabels is supported) + +```yaml +spec: + kubernetes: + selector: + matchLabels: + someKey: someValue + other-key: other-value + app.kubernetes.io/name: dice-roller +``` diff --git a/plugins/kubernetes/src/api/KubernetesBackendClient.ts b/plugins/kubernetes/src/api/KubernetesBackendClient.ts index 86bb830046..faf9aaf024 100644 --- a/plugins/kubernetes/src/api/KubernetesBackendClient.ts +++ b/plugins/kubernetes/src/api/KubernetesBackendClient.ts @@ -20,6 +20,7 @@ import { AuthRequestBody, ObjectsByServiceIdResponse, } from '@backstage/plugin-kubernetes-backend'; +import { V1LabelSelector } from '@kubernetes/client-node'; export class KubernetesBackendClient implements KubernetesApi { private readonly discoveryApi: DiscoveryApi; @@ -50,10 +51,23 @@ export class KubernetesBackendClient implements KubernetesApi { return await response.json(); } - async getObjectsByServiceId( + private parseLabelSelector(params: V1LabelSelector): string { + // TODO: figure out how to convert the selector to the full query param from the yaml + // (as shown here https://github.com/kubernetes/apimachinery/blob/master/pkg/labels/selector.go) + return Object.keys(params.matchLabels) + .map(key => `${key}=${params[key]}`) + .join(','); + } + + async getObjectsByLabelSelector( serviceId: String, + labelSelector: V1LabelSelector, requestBody: AuthRequestBody, ): Promise { - return await this.getRequired(`/services/${serviceId}`, requestBody); + const labelSelectorQueryParams = this.parseLabelSelector(labelSelector); + return await this.getRequired( + `/services/${serviceId}?labelSelector=${labelSelectorQueryParams}`, + requestBody, + ); } } diff --git a/plugins/kubernetes/src/api/types.ts b/plugins/kubernetes/src/api/types.ts index 8e44d58e6a..2f13dcb02b 100644 --- a/plugins/kubernetes/src/api/types.ts +++ b/plugins/kubernetes/src/api/types.ts @@ -19,6 +19,7 @@ import { AuthRequestBody, ObjectsByServiceIdResponse, } from '@backstage/plugin-kubernetes-backend'; +import { V1LabelSelector } from '@kubernetes/client-node'; export const kubernetesApiRef = createApiRef({ id: 'plugin.kubernetes.service', @@ -27,8 +28,9 @@ export const kubernetesApiRef = createApiRef({ }); export interface KubernetesApi { - getObjectsByServiceId( + getObjectsByLabelSelector( serviceId: String, + labelSelector: V1LabelSelector, requestBody: AuthRequestBody, ): Promise; } diff --git a/plugins/kubernetes/src/components/KubernetesContent/KubernetesContent.tsx b/plugins/kubernetes/src/components/KubernetesContent/KubernetesContent.tsx index a3e82e79a4..d46a888b39 100644 --- a/plugins/kubernetes/src/components/KubernetesContent/KubernetesContent.tsx +++ b/plugins/kubernetes/src/components/KubernetesContent/KubernetesContent.tsx @@ -41,6 +41,7 @@ import { ExtensionsV1beta1Ingress, V1ConfigMap, V1HorizontalPodAutoscaler, + V1LabelSelector, V1Service, } from '@kubernetes/client-node'; import { Services } from '../Services'; @@ -129,9 +130,27 @@ export const KubernetesContent = ({ entity }: KubernetesContentProps) => { ); } + // decide label selector to search by defaulting to this label + let labelSelector: V1LabelSelector = { + matchLabels: { + 'backstage.io/kubernetes-id': entity.metadata.name, + }, + }; + + if ( + entity.spec.kubernetes && + (entity.spec.kubernetes.selector as V1LabelSelector) + ) { + labelSelector = entity.spec.kubernetes.selector; + } + // TODO: Add validation on contents/format of requestBody kubernetesApi - .getObjectsByServiceId(entity.metadata.name, requestBody) + .getObjectsByLabelSelector( + entity.metadata.name, + labelSelector, + requestBody, + ) .then(result => { setKubernetesObjects(result); })