feat(kubernetes): update k8s-backend plugin to use label selectors
update plugin to use label selectors when querying backend
This commit is contained in:
committed by
moustafab
parent
da2f4cd41a
commit
e2085293a6
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
+5
-5
@@ -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,
|
||||
|
||||
+5
-3
@@ -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>;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user