Merge pull request #2977 from moustafab/feature/k8s-label-selector
feat(kubernetes): update kubernetes plugin to support matchLabels
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
---
|
||||
'@backstage/catalog-model': minor
|
||||
'@backstage/plugin-kubernetes': minor
|
||||
'@backstage/plugin-kubernetes-backend': minor
|
||||
---
|
||||
|
||||
add kubernetes selector to component model
|
||||
@@ -30,6 +30,15 @@ const schema = yup.object<Partial<ComponentEntityV1alpha1>>({
|
||||
lifecycle: yup.string().required().min(1),
|
||||
owner: yup.string().required().min(1),
|
||||
implementsApis: yup.array(yup.string().required()).notRequired(),
|
||||
kubernetes: yup
|
||||
.object<any>({
|
||||
selector: yup
|
||||
.object<any>({
|
||||
matchLabels: yup.object<any>().required(),
|
||||
})
|
||||
.required(),
|
||||
})
|
||||
.notRequired(),
|
||||
})
|
||||
.required(),
|
||||
});
|
||||
@@ -42,6 +51,13 @@ export interface ComponentEntityV1alpha1 extends Entity {
|
||||
lifecycle: string;
|
||||
owner: string;
|
||||
implementsApis?: string[];
|
||||
kubernetes?: {
|
||||
selector: {
|
||||
matchLabels: {
|
||||
[key: string]: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/backend-common": "^0.3.0",
|
||||
"@backstage/catalog-model": "^0.2.0",
|
||||
"@backstage/config": "^0.1.1",
|
||||
"@kubernetes/client-node": "^0.12.1",
|
||||
"@types/express": "^4.17.6",
|
||||
|
||||
+2
-2
@@ -15,13 +15,13 @@
|
||||
*/
|
||||
|
||||
import { KubernetesAuthTranslator } from './types';
|
||||
import { AuthRequestBody, ClusterDetails } from '../types/types';
|
||||
import { KubernetesRequestBody, ClusterDetails } from '../types/types';
|
||||
|
||||
export class GoogleKubernetesAuthTranslator
|
||||
implements KubernetesAuthTranslator {
|
||||
async decorateClusterDetailsWithAuth(
|
||||
clusterDetails: ClusterDetails,
|
||||
requestBody: AuthRequestBody,
|
||||
requestBody: KubernetesRequestBody,
|
||||
): Promise<ClusterDetails> {
|
||||
const clusterDetailsWithAuthToken: ClusterDetails = Object.assign(
|
||||
{},
|
||||
|
||||
+2
-2
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
import { KubernetesAuthTranslator } from './types';
|
||||
import { AuthRequestBody, ClusterDetails } from '../types/types';
|
||||
import { KubernetesRequestBody, ClusterDetails } from '../types/types';
|
||||
|
||||
export class ServiceAccountKubernetesAuthTranslator
|
||||
implements KubernetesAuthTranslator {
|
||||
@@ -23,7 +23,7 @@ export class ServiceAccountKubernetesAuthTranslator
|
||||
clusterDetails: ClusterDetails,
|
||||
// To ignore TS6133 linting error where it detects 'requestBody' is declared but its value is never read.
|
||||
// @ts-ignore-start
|
||||
requestBody: AuthRequestBody, // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||
requestBody: KubernetesRequestBody, // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||
// @ts-ignore-end
|
||||
): Promise<ClusterDetails> {
|
||||
return clusterDetails;
|
||||
|
||||
@@ -14,11 +14,11 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { AuthRequestBody, ClusterDetails } from '../types/types';
|
||||
import { KubernetesRequestBody, ClusterDetails } from '../types/types';
|
||||
|
||||
export interface KubernetesAuthTranslator {
|
||||
decorateClusterDetailsWithAuth(
|
||||
clusterDetails: ClusterDetails,
|
||||
requestBody: AuthRequestBody,
|
||||
requestBody: KubernetesRequestBody,
|
||||
): Promise<ClusterDetails>;
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
import { getVoidLogger } from '@backstage/backend-common';
|
||||
import { KubernetesClientBasedFetcher } from './KubernetesFetcher';
|
||||
import { ObjectFetchParams } from '..';
|
||||
|
||||
describe('KubernetesClientProvider', () => {
|
||||
let clientMock: any;
|
||||
@@ -57,16 +58,17 @@ describe('KubernetesClientProvider', () => {
|
||||
|
||||
clientMock.listServiceForAllNamespaces.mockRejectedValue(errorResponse);
|
||||
|
||||
const result = await sut.fetchObjectsByServiceId(
|
||||
'some-service',
|
||||
{
|
||||
const result = await sut.fetchObjectsForService(<ObjectFetchParams>{
|
||||
serviceId: 'some-service',
|
||||
clusterDetails: {
|
||||
name: 'cluster1',
|
||||
url: 'http://localhost:9999',
|
||||
serviceAccountToken: 'token',
|
||||
authProvider: 'serviceAccount',
|
||||
},
|
||||
new Set(['pods', 'services']),
|
||||
);
|
||||
objectTypesToFetch: new Set(['pods', 'services']),
|
||||
labelSelector: '',
|
||||
});
|
||||
|
||||
expect(result).toStrictEqual({
|
||||
errors: [expectedResult],
|
||||
@@ -120,16 +122,17 @@ describe('KubernetesClientProvider', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const result = await sut.fetchObjectsByServiceId(
|
||||
'some-service',
|
||||
{
|
||||
const result = await sut.fetchObjectsForService(<ObjectFetchParams>{
|
||||
serviceId: 'some-service',
|
||||
clusterDetails: {
|
||||
name: 'cluster1',
|
||||
url: 'http://localhost:9999',
|
||||
serviceAccountToken: 'token',
|
||||
authProvider: 'serviceAccount',
|
||||
},
|
||||
new Set(['pods', 'services']),
|
||||
);
|
||||
objectTypesToFetch: new Set(['pods', 'services']),
|
||||
labelSelector: '',
|
||||
});
|
||||
|
||||
expect(result).toStrictEqual({
|
||||
errors: [],
|
||||
@@ -169,16 +172,17 @@ describe('KubernetesClientProvider', () => {
|
||||
});
|
||||
it('should throw error on unknown type', () => {
|
||||
expect(() =>
|
||||
sut.fetchObjectsByServiceId(
|
||||
'some-service',
|
||||
{
|
||||
sut.fetchObjectsForService(<ObjectFetchParams>{
|
||||
serviceId: 'some-service',
|
||||
clusterDetails: {
|
||||
name: 'cluster1',
|
||||
url: 'http://localhost:9999',
|
||||
serviceAccountToken: 'token',
|
||||
authProvider: 'serviceAccount',
|
||||
},
|
||||
new Set<any>(['foo']),
|
||||
),
|
||||
objectTypesToFetch: new Set<any>(['foo']),
|
||||
labelSelector: '',
|
||||
}),
|
||||
).toThrow('unrecognised type=foo');
|
||||
|
||||
expect(clientMock.listPodForAllNamespaces.mock.calls.length).toBe(0);
|
||||
@@ -254,4 +258,46 @@ describe('KubernetesClientProvider', () => {
|
||||
},
|
||||
);
|
||||
});
|
||||
it('should always add a labelSelector query', async () => {
|
||||
clientMock.listPodForAllNamespaces.mockResolvedValueOnce({
|
||||
body: {
|
||||
items: [
|
||||
{
|
||||
metadata: {
|
||||
name: 'pod-name',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
clientMock.listServiceForAllNamespaces.mockResolvedValueOnce({
|
||||
body: {
|
||||
items: [
|
||||
{
|
||||
metadata: {
|
||||
name: 'service-name',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
await sut.fetchObjectsForService(<ObjectFetchParams>{
|
||||
serviceId: 'some-service',
|
||||
clusterDetails: {
|
||||
name: 'cluster1',
|
||||
url: 'http://localhost:9999',
|
||||
serviceAccountToken: 'token',
|
||||
authProvider: 'serviceAccount',
|
||||
},
|
||||
objectTypesToFetch: new Set(['pods', 'services']),
|
||||
labelSelector: '',
|
||||
});
|
||||
|
||||
const mockCall = clientMock.listPodForAllNamespaces.mock.calls[0];
|
||||
const actualSelector = mockCall[mockCall.length - 1];
|
||||
const expectedSelector = 'backstage.io/kubernetes-id=some-service';
|
||||
expect(actualSelector).toBe(expectedSelector);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -38,6 +38,7 @@ import {
|
||||
FetchResponseWrapper,
|
||||
KubernetesFetchError,
|
||||
KubernetesErrorTypes,
|
||||
ObjectFetchParams,
|
||||
} from '..';
|
||||
import lodash, { Dictionary } from 'lodash';
|
||||
|
||||
@@ -105,15 +106,16 @@ export class KubernetesClientBasedFetcher implements KubernetesFetcher {
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
fetchObjectsByServiceId(
|
||||
serviceId: string,
|
||||
clusterDetails: ClusterDetails,
|
||||
objectTypesToFetch: Set<KubernetesObjectTypes>,
|
||||
fetchObjectsForService(
|
||||
params: ObjectFetchParams,
|
||||
): Promise<FetchResponseWrapper> {
|
||||
const fetchResults = Array.from(objectTypesToFetch).map(type => {
|
||||
return this.fetchByObjectType(serviceId, clusterDetails, type).catch(
|
||||
captureKubernetesErrorsRethrowOthers,
|
||||
);
|
||||
const fetchResults = Array.from(params.objectTypesToFetch).map(type => {
|
||||
return this.fetchByObjectType(
|
||||
params.clusterDetails,
|
||||
type,
|
||||
params.labelSelector ||
|
||||
`backstage.io/kubernetes-id=${params.serviceId}`,
|
||||
).catch(captureKubernetesErrorsRethrowOthers);
|
||||
});
|
||||
|
||||
return Promise.all(fetchResults).then(fetchResultsToResponseWrapper);
|
||||
@@ -121,45 +123,47 @@ export class KubernetesClientBasedFetcher implements KubernetesFetcher {
|
||||
|
||||
// TODO could probably do with a tidy up
|
||||
private fetchByObjectType(
|
||||
serviceId: string,
|
||||
clusterDetails: ClusterDetails,
|
||||
type: KubernetesObjectTypes,
|
||||
labelSelector: string,
|
||||
): Promise<FetchResponse> {
|
||||
switch (type) {
|
||||
case 'pods':
|
||||
return this.fetchPodsByServiceId(serviceId, clusterDetails).then(r => ({
|
||||
type: type,
|
||||
resources: r,
|
||||
}));
|
||||
return this.fetchPodsForService(clusterDetails, labelSelector).then(
|
||||
r => ({
|
||||
type: type,
|
||||
resources: r,
|
||||
}),
|
||||
);
|
||||
case 'configmaps':
|
||||
return this.fetchConfigMapsByServiceId(
|
||||
serviceId,
|
||||
return this.fetchConfigMapsForService(
|
||||
clusterDetails,
|
||||
labelSelector,
|
||||
).then(r => ({ type: type, resources: r }));
|
||||
case 'deployments':
|
||||
return this.fetchDeploymentsByServiceId(
|
||||
serviceId,
|
||||
return this.fetchDeploymentsForService(
|
||||
clusterDetails,
|
||||
labelSelector,
|
||||
).then(r => ({ type: type, resources: r }));
|
||||
case 'replicasets':
|
||||
return this.fetchReplicaSetsByServiceId(
|
||||
serviceId,
|
||||
return this.fetchReplicaSetsForService(
|
||||
clusterDetails,
|
||||
labelSelector,
|
||||
).then(r => ({ type: type, resources: r }));
|
||||
case 'services':
|
||||
return this.fetchServicesByServiceId(
|
||||
serviceId,
|
||||
return this.fetchServicesForService(
|
||||
clusterDetails,
|
||||
labelSelector,
|
||||
).then(r => ({ type: type, resources: r }));
|
||||
case 'horizontalpodautoscalers':
|
||||
return this.fetchHorizontalPodAutoscalersByServiceId(
|
||||
serviceId,
|
||||
return this.fetchHorizontalPodAutoscalersForService(
|
||||
clusterDetails,
|
||||
labelSelector,
|
||||
).then(r => ({ type: type, resources: r }));
|
||||
case 'ingresses':
|
||||
return this.fetchIngressesByServiceId(
|
||||
serviceId,
|
||||
return this.fetchIngressesForService(
|
||||
clusterDetails,
|
||||
labelSelector,
|
||||
).then(r => ({ type: type, resources: r }));
|
||||
default:
|
||||
// unrecognised type
|
||||
@@ -192,79 +196,54 @@ export class KubernetesClientBasedFetcher implements KubernetesFetcher {
|
||||
});
|
||||
}
|
||||
|
||||
private fetchServicesByServiceId(
|
||||
serviceId: string,
|
||||
private fetchServicesForService(
|
||||
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(
|
||||
serviceId: string,
|
||||
private fetchPodsForService(
|
||||
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(
|
||||
serviceId: string,
|
||||
private fetchConfigMapsForService(
|
||||
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(
|
||||
serviceId: string,
|
||||
private fetchDeploymentsForService(
|
||||
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(
|
||||
serviceId: string,
|
||||
private fetchReplicaSetsForService(
|
||||
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(
|
||||
serviceId: string,
|
||||
private fetchHorizontalPodAutoscalersForService(
|
||||
clusterDetails: ClusterDetails,
|
||||
labelSelector: string,
|
||||
): Promise<Array<V1HorizontalPodAutoscaler>> {
|
||||
return this.singleClusterFetch<V1HorizontalPodAutoscaler>(
|
||||
clusterDetails,
|
||||
@@ -273,14 +252,14 @@ export class KubernetesClientBasedFetcher implements KubernetesFetcher {
|
||||
false,
|
||||
'',
|
||||
'',
|
||||
`backstage.io/kubernetes-id=${serviceId}`,
|
||||
labelSelector,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
private fetchIngressesByServiceId(
|
||||
serviceId: string,
|
||||
private fetchIngressesForService(
|
||||
clusterDetails: ClusterDetails,
|
||||
labelSelector: string,
|
||||
): Promise<Array<ExtensionsV1beta1Ingress>> {
|
||||
return this.singleClusterFetch<ExtensionsV1beta1Ingress>(
|
||||
clusterDetails,
|
||||
@@ -289,7 +268,7 @@ export class KubernetesClientBasedFetcher implements KubernetesFetcher {
|
||||
false,
|
||||
'',
|
||||
'',
|
||||
`backstage.io/kubernetes-id=${serviceId}`,
|
||||
labelSelector,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
+39
-17
@@ -14,18 +14,39 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { handleGetKubernetesObjectsByServiceId } from './getKubernetesObjectsByServiceIdHandler';
|
||||
import { handleGetKubernetesObjectsForService } from './getKubernetesObjectsForServiceHandler';
|
||||
import { getVoidLogger } from '@backstage/backend-common';
|
||||
import { ClusterDetails } from '..';
|
||||
import { ComponentEntityV1alpha1 } from '@backstage/catalog-model';
|
||||
import { ObjectFetchParams } from '..';
|
||||
|
||||
const TEST_SERVICE_ID = 'my-service';
|
||||
|
||||
const fetchObjectsByServiceId = jest.fn();
|
||||
const fetchObjectsForService = jest.fn();
|
||||
|
||||
const getClustersByServiceId = jest.fn();
|
||||
|
||||
const goodEntity: ComponentEntityV1alpha1 = {
|
||||
apiVersion: 'backstage.io/v1beta1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'test-component',
|
||||
},
|
||||
spec: {
|
||||
type: 'service',
|
||||
lifecycle: 'production',
|
||||
owner: 'joe',
|
||||
kubernetes: {
|
||||
selector: {
|
||||
matchLabels: {
|
||||
'backstage.io/test-label': 'test-component',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const mockFetch = (mock: jest.Mock) => {
|
||||
mock.mockImplementation((serviceId: string, clusterDetails: ClusterDetails) =>
|
||||
mock.mockImplementation((params: ObjectFetchParams) =>
|
||||
Promise.resolve({
|
||||
errors: [],
|
||||
responses: [
|
||||
@@ -34,7 +55,7 @@ const mockFetch = (mock: jest.Mock) => {
|
||||
resources: [
|
||||
{
|
||||
metadata: {
|
||||
name: `my-pods-${serviceId}-${clusterDetails.name}`,
|
||||
name: `my-pods-${params.serviceId}-${params.clusterDetails.name}`,
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -44,7 +65,7 @@ const mockFetch = (mock: jest.Mock) => {
|
||||
resources: [
|
||||
{
|
||||
metadata: {
|
||||
name: `my-configmaps-${serviceId}-${clusterDetails.name}`,
|
||||
name: `my-configmaps-${params.serviceId}-${params.clusterDetails.name}`,
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -54,7 +75,7 @@ const mockFetch = (mock: jest.Mock) => {
|
||||
resources: [
|
||||
{
|
||||
metadata: {
|
||||
name: `my-services-${serviceId}-${clusterDetails.name}`,
|
||||
name: `my-services-${params.serviceId}-${params.clusterDetails.name}`,
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -64,7 +85,7 @@ const mockFetch = (mock: jest.Mock) => {
|
||||
);
|
||||
};
|
||||
|
||||
describe('handleGetKubernetesObjectsByServiceId', () => {
|
||||
describe('handleGetKubernetesObjectsForService', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
@@ -79,22 +100,22 @@ describe('handleGetKubernetesObjectsByServiceId', () => {
|
||||
]),
|
||||
);
|
||||
|
||||
mockFetch(fetchObjectsByServiceId);
|
||||
mockFetch(fetchObjectsForService);
|
||||
|
||||
const result = await handleGetKubernetesObjectsByServiceId(
|
||||
const result = await handleGetKubernetesObjectsForService(
|
||||
TEST_SERVICE_ID,
|
||||
{
|
||||
fetchObjectsByServiceId,
|
||||
fetchObjectsForService: fetchObjectsForService,
|
||||
},
|
||||
{
|
||||
getClustersByServiceId,
|
||||
},
|
||||
getVoidLogger(),
|
||||
{},
|
||||
{ entity: goodEntity },
|
||||
);
|
||||
|
||||
expect(getClustersByServiceId.mock.calls.length).toBe(1);
|
||||
expect(fetchObjectsByServiceId.mock.calls.length).toBe(1);
|
||||
expect(fetchObjectsForService.mock.calls.length).toBe(1);
|
||||
expect(result).toStrictEqual({
|
||||
items: [
|
||||
{
|
||||
@@ -153,18 +174,19 @@ describe('handleGetKubernetesObjectsByServiceId', () => {
|
||||
]),
|
||||
);
|
||||
|
||||
mockFetch(fetchObjectsByServiceId);
|
||||
mockFetch(fetchObjectsForService);
|
||||
|
||||
const result = await handleGetKubernetesObjectsByServiceId(
|
||||
const result = await handleGetKubernetesObjectsForService(
|
||||
TEST_SERVICE_ID,
|
||||
{
|
||||
fetchObjectsByServiceId,
|
||||
fetchObjectsForService: fetchObjectsForService,
|
||||
},
|
||||
{
|
||||
getClustersByServiceId,
|
||||
},
|
||||
getVoidLogger(),
|
||||
{
|
||||
entity: goodEntity,
|
||||
auth: {
|
||||
google: 'google_token_123',
|
||||
},
|
||||
@@ -172,7 +194,7 @@ describe('handleGetKubernetesObjectsByServiceId', () => {
|
||||
);
|
||||
|
||||
expect(getClustersByServiceId.mock.calls.length).toBe(1);
|
||||
expect(fetchObjectsByServiceId.mock.calls.length).toBe(2);
|
||||
expect(fetchObjectsForService.mock.calls.length).toBe(2);
|
||||
expect(result).toStrictEqual({
|
||||
items: [
|
||||
{
|
||||
|
||||
+32
-11
@@ -15,25 +15,27 @@
|
||||
*/
|
||||
|
||||
import { Logger } from 'winston';
|
||||
import { ComponentEntityV1alpha1 } from '@backstage/catalog-model';
|
||||
import {
|
||||
AuthRequestBody,
|
||||
KubernetesRequestBody,
|
||||
ClusterDetails,
|
||||
KubernetesServiceLocator,
|
||||
KubernetesFetcher,
|
||||
KubernetesObjectTypes,
|
||||
ObjectsByServiceIdResponse,
|
||||
ObjectsByEntityResponse,
|
||||
ObjectFetchParams,
|
||||
} from '../types/types';
|
||||
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,
|
||||
objectsToFetch?: Set<KubernetesObjectTypes>,
|
||||
) => Promise<ObjectsByServiceIdResponse>;
|
||||
requestBody: KubernetesRequestBody,
|
||||
objectTypesToFetch?: Set<KubernetesObjectTypes>,
|
||||
) => Promise<ObjectsByEntityResponse>;
|
||||
|
||||
const DEFAULT_OBJECTS = new Set<KubernetesObjectTypes>([
|
||||
'pods',
|
||||
@@ -45,14 +47,26 @@ const DEFAULT_OBJECTS = new Set<KubernetesObjectTypes>([
|
||||
'ingresses',
|
||||
]);
|
||||
|
||||
function parseLabelSelector(entity: ComponentEntityV1alpha1): string {
|
||||
const matchLabels = entity?.spec?.kubernetes?.selector?.matchLabels;
|
||||
if (matchLabels) {
|
||||
// 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(matchLabels)
|
||||
.map(key => `${key}=${matchLabels[key.toString()]}`)
|
||||
.join(',');
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
// 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,
|
||||
objectsToFetch = DEFAULT_OBJECTS,
|
||||
objectTypesToFetch = DEFAULT_OBJECTS,
|
||||
) => {
|
||||
const clusterDetails: ClusterDetails[] = await serviceLocator.getClustersByServiceId(
|
||||
serviceId,
|
||||
@@ -78,14 +92,21 @@ export const handleGetKubernetesObjectsByServiceId: GetKubernetesObjectsByServic
|
||||
.join(', ')}]`,
|
||||
);
|
||||
|
||||
const labelSelector = parseLabelSelector(requestBody.entity);
|
||||
|
||||
return Promise.all(
|
||||
clusterDetailsDecoratedForAuth.map(cd => {
|
||||
clusterDetailsDecoratedForAuth.map(clusterDetails => {
|
||||
return fetcher
|
||||
.fetchObjectsByServiceId(serviceId, cd, objectsToFetch)
|
||||
.fetchObjectsForService(<ObjectFetchParams>{
|
||||
serviceId,
|
||||
clusterDetails,
|
||||
objectTypesToFetch,
|
||||
labelSelector,
|
||||
})
|
||||
.then(result => {
|
||||
return {
|
||||
cluster: {
|
||||
name: cd.name,
|
||||
name: clusterDetails.name,
|
||||
},
|
||||
resources: result.responses,
|
||||
errors: result.errors,
|
||||
@@ -21,18 +21,18 @@ import { makeRouter } from './router';
|
||||
import {
|
||||
KubernetesServiceLocator,
|
||||
KubernetesFetcher,
|
||||
ObjectsByServiceIdResponse,
|
||||
ObjectsByEntityResponse,
|
||||
} from '..';
|
||||
|
||||
describe('router', () => {
|
||||
let app: express.Express;
|
||||
let kubernetesFetcher: jest.Mocked<KubernetesFetcher>;
|
||||
let kubernetesServiceLocator: jest.Mocked<KubernetesServiceLocator>;
|
||||
let handleGetByServiceId: jest.Mock<Promise<ObjectsByServiceIdResponse>>;
|
||||
let handleGetByServiceId: jest.Mock<Promise<ObjectsByEntityResponse>>;
|
||||
|
||||
beforeAll(async () => {
|
||||
kubernetesFetcher = {
|
||||
fetchObjectsByServiceId: jest.fn(),
|
||||
fetchObjectsForService: jest.fn(),
|
||||
};
|
||||
|
||||
kubernetesServiceLocator = {
|
||||
|
||||
@@ -22,11 +22,11 @@ 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,
|
||||
KubernetesRequestBody,
|
||||
KubernetesServiceLocator,
|
||||
KubernetesFetcher,
|
||||
ServiceLocatorMethod,
|
||||
@@ -64,16 +64,16 @@ export const makeRouter = (
|
||||
logger: Logger,
|
||||
fetcher: KubernetesFetcher,
|
||||
serviceLocator: KubernetesServiceLocator,
|
||||
handleGetByServiceId: GetKubernetesObjectsByServiceIdHandler,
|
||||
handleGetByEntity: GetKubernetesObjectsForServiceHandler,
|
||||
): express.Router => {
|
||||
const router = Router();
|
||||
router.use(express.json());
|
||||
|
||||
router.post('/services/:serviceId', async (req, res) => {
|
||||
const serviceId = req.params.serviceId;
|
||||
const requestBody: AuthRequestBody = req.body;
|
||||
const requestBody: KubernetesRequestBody = req.body;
|
||||
try {
|
||||
const response = await handleGetByServiceId(
|
||||
const response = await handleGetByEntity(
|
||||
serviceId,
|
||||
fetcher,
|
||||
serviceLocator,
|
||||
@@ -119,6 +119,6 @@ export async function createRouter(
|
||||
logger,
|
||||
fetcher,
|
||||
serviceLocator,
|
||||
handleGetKubernetesObjectsByServiceId,
|
||||
handleGetKubernetesObjectsForService,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
V1ReplicaSet,
|
||||
V1Service,
|
||||
} from '@kubernetes/client-node';
|
||||
import { ComponentEntityV1alpha1 } from '@backstage/catalog-model';
|
||||
|
||||
export interface ClusterDetails {
|
||||
name: string;
|
||||
@@ -31,10 +32,11 @@ export interface ClusterDetails {
|
||||
serviceAccountToken?: string | undefined;
|
||||
}
|
||||
|
||||
export interface AuthRequestBody {
|
||||
export interface KubernetesRequestBody {
|
||||
auth?: {
|
||||
google?: string;
|
||||
};
|
||||
entity: ComponentEntityV1alpha1;
|
||||
}
|
||||
|
||||
export interface ClusterObjects {
|
||||
@@ -43,7 +45,7 @@ export interface ClusterObjects {
|
||||
errors: KubernetesFetchError[];
|
||||
}
|
||||
|
||||
export interface ObjectsByServiceIdResponse {
|
||||
export interface ObjectsByEntityResponse {
|
||||
items: ClusterObjects[];
|
||||
}
|
||||
|
||||
@@ -107,13 +109,18 @@ export interface IngressesFetchResponse {
|
||||
resources: Array<ExtensionsV1beta1Ingress>;
|
||||
}
|
||||
|
||||
export interface ObjectFetchParams {
|
||||
serviceId: string;
|
||||
clusterDetails: ClusterDetails;
|
||||
objectTypesToFetch: Set<KubernetesObjectTypes>;
|
||||
labelSelector: string;
|
||||
}
|
||||
|
||||
// Fetches information from a kubernetes cluster using the cluster details object
|
||||
// to target a specific cluster
|
||||
export interface KubernetesFetcher {
|
||||
fetchObjectsByServiceId(
|
||||
serviceId: string,
|
||||
clusterDetails: ClusterDetails,
|
||||
objectTypesToFetch: Set<KubernetesObjectTypes>,
|
||||
fetchObjectsForService(
|
||||
params: ObjectFetchParams,
|
||||
): Promise<FetchResponseWrapper>;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,28 @@ 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 two ways to surface your kubernetes components as part of an entity.
|
||||
The label selector takes precedence over the annotation/service 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
|
||||
```
|
||||
|
||||
### Common `backstage.io/kubernetes-id` label on objects
|
||||
|
||||
#### 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 +45,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:
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
import { DiscoveryApi } from '@backstage/core';
|
||||
import { KubernetesApi } from './types';
|
||||
import {
|
||||
AuthRequestBody,
|
||||
ObjectsByServiceIdResponse,
|
||||
KubernetesRequestBody,
|
||||
ObjectsByEntityResponse,
|
||||
} from '@backstage/plugin-kubernetes-backend';
|
||||
|
||||
export class KubernetesBackendClient implements KubernetesApi {
|
||||
@@ -30,7 +30,7 @@ export class KubernetesBackendClient implements KubernetesApi {
|
||||
|
||||
private async getRequired(
|
||||
path: string,
|
||||
requestBody: AuthRequestBody,
|
||||
requestBody: KubernetesRequestBody,
|
||||
): Promise<any> {
|
||||
const url = `${await this.discoveryApi.getBaseUrl('kubernetes')}${path}`;
|
||||
const response = await fetch(url, {
|
||||
@@ -50,10 +50,12 @@ export class KubernetesBackendClient implements KubernetesApi {
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
async getObjectsByServiceId(
|
||||
serviceId: String,
|
||||
requestBody: AuthRequestBody,
|
||||
): Promise<ObjectsByServiceIdResponse> {
|
||||
return await this.getRequired(`/services/${serviceId}`, requestBody);
|
||||
async getObjectsByEntity(
|
||||
requestBody: KubernetesRequestBody,
|
||||
): Promise<ObjectsByEntityResponse> {
|
||||
return await this.getRequired(
|
||||
`/services/${requestBody.entity.metadata.name}`,
|
||||
requestBody,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
|
||||
import { createApiRef } from '@backstage/core';
|
||||
import {
|
||||
AuthRequestBody,
|
||||
ObjectsByServiceIdResponse,
|
||||
KubernetesRequestBody,
|
||||
ObjectsByEntityResponse,
|
||||
} from '@backstage/plugin-kubernetes-backend';
|
||||
|
||||
export const kubernetesApiRef = createApiRef<KubernetesApi>({
|
||||
@@ -27,8 +27,7 @@ export const kubernetesApiRef = createApiRef<KubernetesApi>({
|
||||
});
|
||||
|
||||
export interface KubernetesApi {
|
||||
getObjectsByServiceId(
|
||||
serviceId: String,
|
||||
requestBody: AuthRequestBody,
|
||||
): Promise<ObjectsByServiceIdResponse>;
|
||||
getObjectsByEntity(
|
||||
requestBody: KubernetesRequestBody,
|
||||
): Promise<ObjectsByEntityResponse>;
|
||||
}
|
||||
|
||||
@@ -26,13 +26,13 @@ import {
|
||||
TabbedCard,
|
||||
useApi,
|
||||
} from '@backstage/core';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { ComponentEntityV1alpha1, Entity } from '@backstage/catalog-model';
|
||||
import { kubernetesApiRef } from '../../api/types';
|
||||
import {
|
||||
AuthRequestBody,
|
||||
KubernetesRequestBody,
|
||||
ClusterObjects,
|
||||
FetchResponse,
|
||||
ObjectsByServiceIdResponse,
|
||||
ObjectsByEntityResponse,
|
||||
} from '@backstage/plugin-kubernetes-backend';
|
||||
import { kubernetesAuthProvidersApiRef } from '../../kubernetes-auth-provider/types';
|
||||
import { DeploymentTables } from '../DeploymentTables';
|
||||
@@ -104,7 +104,7 @@ export const KubernetesContent = ({ entity }: KubernetesContentProps) => {
|
||||
const kubernetesApi = useApi(kubernetesApiRef);
|
||||
|
||||
const [kubernetesObjects, setKubernetesObjects] = useState<
|
||||
ObjectsByServiceIdResponse | undefined
|
||||
ObjectsByEntityResponse | undefined
|
||||
>(undefined);
|
||||
const [error, setError] = useState<string | undefined>(undefined);
|
||||
|
||||
@@ -120,7 +120,9 @@ export const KubernetesContent = ({ entity }: KubernetesContentProps) => {
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
// For each auth type, invoke decorateRequestBodyForAuth on corresponding KubernetesAuthProvider
|
||||
let requestBody: AuthRequestBody = {};
|
||||
let requestBody: KubernetesRequestBody = {
|
||||
entity: entity as ComponentEntityV1alpha1,
|
||||
};
|
||||
for (const authProviderStr of authProviders) {
|
||||
// Multiple asyncs done sequentially instead of all at once to prevent same requestBody from being modified simultaneously
|
||||
requestBody = await kubernetesAuthProvidersApi.decorateRequestBodyForAuth(
|
||||
@@ -131,7 +133,7 @@ export const KubernetesContent = ({ entity }: KubernetesContentProps) => {
|
||||
|
||||
// TODO: Add validation on contents/format of requestBody
|
||||
kubernetesApi
|
||||
.getObjectsByServiceId(entity.metadata.name, requestBody)
|
||||
.getObjectsByEntity(requestBody)
|
||||
.then(result => {
|
||||
setKubernetesObjects(result);
|
||||
})
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
import { OAuthApi } from '@backstage/core';
|
||||
import { KubernetesAuthProvider } from './types';
|
||||
import { AuthRequestBody } from '@backstage/plugin-kubernetes-backend';
|
||||
import { KubernetesRequestBody } from '@backstage/plugin-kubernetes-backend';
|
||||
|
||||
export class GoogleKubernetesAuthProvider implements KubernetesAuthProvider {
|
||||
authProvider: OAuthApi;
|
||||
@@ -26,8 +26,8 @@ export class GoogleKubernetesAuthProvider implements KubernetesAuthProvider {
|
||||
}
|
||||
|
||||
async decorateRequestBodyForAuth(
|
||||
requestBody: AuthRequestBody,
|
||||
): Promise<AuthRequestBody> {
|
||||
requestBody: KubernetesRequestBody,
|
||||
): Promise<KubernetesRequestBody> {
|
||||
const googleAuthToken: string = await this.authProvider.getAccessToken(
|
||||
'https://www.googleapis.com/auth/cloud-platform',
|
||||
);
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
import { OAuthApi } from '@backstage/core';
|
||||
import { AuthRequestBody } from '@backstage/plugin-kubernetes-backend';
|
||||
import { KubernetesRequestBody } from '@backstage/plugin-kubernetes-backend';
|
||||
import { KubernetesAuthProvider, KubernetesAuthProvidersApi } from './types';
|
||||
import { GoogleKubernetesAuthProvider } from './GoogleKubernetesAuthProvider';
|
||||
import { ServiceAccountKubernetesAuthProvider } from './ServiceAccountKubernetesAuthProvider';
|
||||
@@ -40,8 +40,8 @@ export class KubernetesAuthProviders implements KubernetesAuthProvidersApi {
|
||||
|
||||
async decorateRequestBodyForAuth(
|
||||
authProvider: string,
|
||||
requestBody: AuthRequestBody,
|
||||
): Promise<AuthRequestBody> {
|
||||
requestBody: KubernetesRequestBody,
|
||||
): Promise<KubernetesRequestBody> {
|
||||
const kubernetesAuthProvider:
|
||||
| KubernetesAuthProvider
|
||||
| undefined = this.kubernetesAuthProviderMap.get(authProvider);
|
||||
|
||||
+3
-3
@@ -15,13 +15,13 @@
|
||||
*/
|
||||
|
||||
import { KubernetesAuthProvider } from './types';
|
||||
import { AuthRequestBody } from '@backstage/plugin-kubernetes-backend';
|
||||
import { KubernetesRequestBody } from '@backstage/plugin-kubernetes-backend';
|
||||
|
||||
export class ServiceAccountKubernetesAuthProvider
|
||||
implements KubernetesAuthProvider {
|
||||
async decorateRequestBodyForAuth(
|
||||
requestBody: AuthRequestBody,
|
||||
): Promise<AuthRequestBody> {
|
||||
requestBody: KubernetesRequestBody,
|
||||
): Promise<KubernetesRequestBody> {
|
||||
// No-op, with service account for auth, cluster config/details should already have serviceAccountToken
|
||||
return requestBody;
|
||||
}
|
||||
|
||||
@@ -15,12 +15,12 @@
|
||||
*/
|
||||
|
||||
import { createApiRef } from '@backstage/core';
|
||||
import { AuthRequestBody } from '@backstage/plugin-kubernetes-backend';
|
||||
import { KubernetesRequestBody } from '@backstage/plugin-kubernetes-backend';
|
||||
|
||||
export interface KubernetesAuthProvider {
|
||||
decorateRequestBodyForAuth(
|
||||
requestBody: AuthRequestBody,
|
||||
): Promise<AuthRequestBody>;
|
||||
requestBody: KubernetesRequestBody,
|
||||
): Promise<KubernetesRequestBody>;
|
||||
}
|
||||
|
||||
export const kubernetesAuthProvidersApiRef = createApiRef<
|
||||
@@ -33,6 +33,6 @@ export const kubernetesAuthProvidersApiRef = createApiRef<
|
||||
export interface KubernetesAuthProvidersApi {
|
||||
decorateRequestBodyForAuth(
|
||||
authProvider: string,
|
||||
requestBody: AuthRequestBody,
|
||||
): Promise<AuthRequestBody>;
|
||||
requestBody: KubernetesRequestBody,
|
||||
): Promise<KubernetesRequestBody>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user