feat(kubernetes): update k8s-backend plugin to use label selectors

update plugin to use label selectors when querying backend
This commit is contained in:
Moustafa Baiou
2020-10-19 23:22:08 -04:00
committed by moustafab
parent da2f4cd41a
commit e2085293a6
11 changed files with 132 additions and 74 deletions
@@ -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',
@@ -105,15 +105,19 @@ export class KubernetesClientBasedFetcher implements KubernetesFetcher {
this.logger = logger;
}
fetchObjectsByServiceId(
fetchObjectsForService(
serviceId: string,
clusterDetails: ClusterDetails,
objectTypesToFetch: Set<KubernetesObjectTypes>,
labelSelector: string,
): Promise<FetchResponseWrapper> {
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<FetchResponse> {
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<Array<V1Service>> {
return this.singleClusterFetch<V1Service>(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<Array<V1Pod>> {
return this.singleClusterFetch<V1Pod>(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<Array<V1ConfigMap>> {
return this.singleClusterFetch<V1Pod>(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<Array<V1Deployment>> {
return this.singleClusterFetch<V1Deployment>(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<Array<V1ReplicaSet>> {
return this.singleClusterFetch<V1ReplicaSet>(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<Array<V1HorizontalPodAutoscaler>> {
return this.singleClusterFetch<V1HorizontalPodAutoscaler>(
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<Array<ExtensionsV1beta1Ingress>> {
return this.singleClusterFetch<ExtensionsV1beta1Ingress>(
clusterDetails,
@@ -289,7 +286,7 @@ export class KubernetesClientBasedFetcher implements KubernetesFetcher {
false,
'',
'',
`backstage.io/kubernetes-id=${serviceId}`,
labelSelector,
),
);
}
@@ -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,
@@ -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<KubernetesObjectTypes>,
) => Promise<ObjectsByServiceIdResponse>;
@@ -46,12 +47,13 @@ const DEFAULT_OBJECTS = new Set<KubernetesObjectTypes>([
]);
// 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: {
@@ -32,7 +32,7 @@ describe('router', () => {
beforeAll(async () => {
kubernetesFetcher = {
fetchObjectsByServiceId: jest.fn(),
fetchObjectsForService: jest.fn(),
};
kubernetesServiceLocator = {
@@ -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,
);
}
@@ -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<KubernetesObjectTypes>,
labelSelector: string,
): Promise<FetchResponseWrapper>;
}
+23 -2
View File
@@ -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': <ENTITY_NAME>
```
### 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
```
@@ -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<ObjectsByServiceIdResponse> {
return await this.getRequired(`/services/${serviceId}`, requestBody);
const labelSelectorQueryParams = this.parseLabelSelector(labelSelector);
return await this.getRequired(
`/services/${serviceId}?labelSelector=${labelSelectorQueryParams}`,
requestBody,
);
}
}
+3 -1
View File
@@ -19,6 +19,7 @@ import {
AuthRequestBody,
ObjectsByServiceIdResponse,
} from '@backstage/plugin-kubernetes-backend';
import { V1LabelSelector } from '@kubernetes/client-node';
export const kubernetesApiRef = createApiRef<KubernetesApi>({
id: 'plugin.kubernetes.service',
@@ -27,8 +28,9 @@ export const kubernetesApiRef = createApiRef<KubernetesApi>({
});
export interface KubernetesApi {
getObjectsByServiceId(
getObjectsByLabelSelector(
serviceId: String,
labelSelector: V1LabelSelector,
requestBody: AuthRequestBody,
): Promise<ObjectsByServiceIdResponse>;
}
@@ -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);
})