Merge pull request #2977 from moustafab/feature/k8s-label-selector

feat(kubernetes): update kubernetes plugin to support matchLabels
This commit is contained in:
Ben Lambert
2020-11-23 16:15:29 +01:00
committed by GitHub
21 changed files with 296 additions and 173 deletions
+7
View File
@@ -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;
};
};
};
};
}
+1
View File
@@ -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",
@@ -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(
{},
@@ -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,
),
);
}
@@ -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: [
{
@@ -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,
);
}
+13 -6
View File
@@ -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>;
}
+23 -2
View File
@@ -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,
);
}
}
+5 -6
View File
@@ -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);
@@ -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>;
}