Kubernetes: initial CRD framework (#4892)

* Kubernetes: initial CRD framework

Signed-off-by: mclarke <mclarke@spotify.com>

* use luxon instead of date-fns

Signed-off-by: mclarke <mclarke@spotify.com>

* fix test

Signed-off-by: mclarke <mclarke@spotify.com>

* update ui schema

Signed-off-by: mclarke <mclarke@spotify.com>

* update app config

Signed-off-by: mclarke <mclarke@spotify.com>

* rollout tests

Signed-off-by: mclarke <mclarke@spotify.com>

* add default custom resource component

Signed-off-by: mclarke <mclarke@spotify.com>

* make custom resources optional

Signed-off-by: mclarke <mclarke@spotify.com>

* add docs

Signed-off-by: mclarke <mclarke@spotify.com>

* prettier

Signed-off-by: mclarke <mclarke@spotify.com>

* replace core-api usage with core

Signed-off-by: mclarke <mclarke@spotify.com>
This commit is contained in:
Matthew Clarke
2021-03-11 10:32:41 +00:00
committed by GitHub
parent 6bd417b7b8
commit e2c1b3fb6a
53 changed files with 5853 additions and 198 deletions
+2 -1
View File
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { ClusterLocatorMethod } from './src/types';
import { ClusterLocatorMethod, CustomResource } from './src/types';
export interface Config {
kubernetes?: {
@@ -22,5 +22,6 @@ export interface Config {
type: 'multiTenant';
};
clusterLocatorMethods: ClusterLocatorMethod[];
customResources?: CustomResource[];
};
}
@@ -20,6 +20,7 @@ import {
CoreV1Api,
KubeConfig,
NetworkingV1beta1Api,
CustomObjectsApi,
} from '@kubernetes/client-node';
import { ClusterDetails } from '../types/types';
@@ -78,4 +79,10 @@ export class KubernetesClientProvider {
return kc.makeApiClient(NetworkingV1beta1Api);
}
getCustomObjectsClient(clusterDetails: ClusterDetails) {
const kc = this.getKubeConfig(clusterDetails);
return kc.makeApiClient(CustomObjectsApi);
}
}
@@ -87,6 +87,7 @@ describe('handleGetKubernetesObjectsForService', () => {
{
getClustersByServiceId,
},
[],
);
const result = await sut.getKubernetesObjectsByEntity({
@@ -178,6 +179,7 @@ describe('handleGetKubernetesObjectsForService', () => {
{
getClustersByServiceId,
},
[],
);
const result = await sut.getKubernetesObjectsByEntity({
@@ -17,6 +17,7 @@
import { Logger } from 'winston';
import {
ClusterDetails,
CustomResource,
KubernetesFetcher,
KubernetesObjectTypes,
KubernetesRequestBody,
@@ -39,15 +40,18 @@ export class KubernetesFanOutHandler {
private readonly logger: Logger;
private readonly fetcher: KubernetesFetcher;
private readonly serviceLocator: KubernetesServiceLocator;
private readonly customResources: CustomResource[];
constructor(
logger: Logger,
fetcher: KubernetesFetcher,
serviceLocator: KubernetesServiceLocator,
customResources: CustomResource[],
) {
this.logger = logger;
this.fetcher = fetcher;
this.serviceLocator = serviceLocator;
this.customResources = customResources;
}
async getKubernetesObjectsByEntity(
@@ -93,6 +97,7 @@ export class KubernetesFanOutHandler {
clusterDetails: clusterDetailsItem,
objectTypesToFetch,
labelSelector,
customResources: this.customResources,
})
.then(result => {
return {
@@ -15,10 +15,9 @@
*/
import { getVoidLogger } from '@backstage/backend-common';
import { ObjectFetchParams } from '../types/types';
import { KubernetesClientBasedFetcher } from './KubernetesFetcher';
describe('KubernetesClientProvider', () => {
describe('KubernetesFetcher', () => {
let clientMock: any;
let kubernetesClientProvider: any;
let sut: KubernetesClientBasedFetcher;
@@ -35,6 +34,7 @@ describe('KubernetesClientProvider', () => {
getAppsClientByClusterDetails: jest.fn(() => clientMock),
getAutoscalingClientByClusterDetails: jest.fn(() => clientMock),
getNetworkingBeta1Client: jest.fn(() => clientMock),
getCustomObjectsClient: jest.fn(() => clientMock),
};
sut = new KubernetesClientBasedFetcher({
@@ -58,7 +58,7 @@ describe('KubernetesClientProvider', () => {
clientMock.listServiceForAllNamespaces.mockRejectedValue(errorResponse);
const result = await sut.fetchObjectsForService(<ObjectFetchParams>{
const result = await sut.fetchObjectsForService({
serviceId: 'some-service',
clusterDetails: {
name: 'cluster1',
@@ -68,6 +68,7 @@ describe('KubernetesClientProvider', () => {
},
objectTypesToFetch: new Set(['pods', 'services']),
labelSelector: '',
customResources: [],
});
expect(result).toStrictEqual({
@@ -122,7 +123,7 @@ describe('KubernetesClientProvider', () => {
},
});
const result = await sut.fetchObjectsForService(<ObjectFetchParams>{
const result = await sut.fetchObjectsForService({
serviceId: 'some-service',
clusterDetails: {
name: 'cluster1',
@@ -132,6 +133,7 @@ describe('KubernetesClientProvider', () => {
},
objectTypesToFetch: new Set(['pods', 'services']),
labelSelector: '',
customResources: [],
});
expect(result).toStrictEqual({
@@ -172,7 +174,7 @@ describe('KubernetesClientProvider', () => {
});
it('should throw error on unknown type', () => {
expect(() =>
sut.fetchObjectsForService(<ObjectFetchParams>{
sut.fetchObjectsForService({
serviceId: 'some-service',
clusterDetails: {
name: 'cluster1',
@@ -182,6 +184,7 @@ describe('KubernetesClientProvider', () => {
},
objectTypesToFetch: new Set<any>(['foo']),
labelSelector: '',
customResources: [],
}),
).toThrow('unrecognised type=foo');
@@ -304,7 +307,7 @@ describe('KubernetesClientProvider', () => {
},
});
await sut.fetchObjectsForService(<ObjectFetchParams>{
await sut.fetchObjectsForService({
serviceId: 'some-service',
clusterDetails: {
name: 'cluster1',
@@ -314,6 +317,7 @@ describe('KubernetesClientProvider', () => {
},
objectTypesToFetch: new Set(['pods', 'services']),
labelSelector: '',
customResources: [],
});
const mockCall = clientMock.listPodForAllNamespaces.mock.calls[0];
@@ -39,6 +39,7 @@ import {
KubernetesFetchError,
KubernetesObjectTypes,
ObjectFetchParams,
CustomResource,
} from '../types/types';
import { KubernetesClientProvider } from './KubernetesClientProvider';
@@ -120,7 +121,18 @@ export class KubernetesClientBasedFetcher implements KubernetesFetcher {
).catch(captureKubernetesErrorsRethrowOthers);
});
return Promise.all(fetchResults).then(fetchResultsToResponseWrapper);
const customObjectsFetchResults = params.customResources.map(cr => {
return this.fetchCustomResource(
params.clusterDetails,
cr,
params.labelSelector ||
`backstage.io/kubernetes-id=${params.serviceId}`,
).catch(captureKubernetesErrorsRethrowOthers);
});
return Promise.all(fetchResults.concat(customObjectsFetchResults)).then(
fetchResultsToResponseWrapper,
);
}
// TODO could probably do with a tidy up
@@ -173,6 +185,30 @@ export class KubernetesClientBasedFetcher implements KubernetesFetcher {
}
}
private fetchCustomResource(
clusterDetails: ClusterDetails,
customResource: CustomResource,
labelSelector: string,
): Promise<FetchResponse> {
const customObjects = this.kubernetesClientProvider.getCustomObjectsClient(
clusterDetails,
);
return customObjects
.listClusterCustomObject(
customResource.group,
customResource.apiVersion,
customResource.plural,
'',
'',
'',
labelSelector,
)
.then(r => {
return { type: 'customresources', resources: (r.body as any).items };
});
}
private singleClusterFetch<T>(
clusterDetails: ClusterDetails,
fn: (
@@ -26,6 +26,7 @@ import {
KubernetesRequestBody,
KubernetesServiceLocator,
ServiceLocatorMethod,
CustomResource,
} from '../types/types';
import { KubernetesClientProvider } from './KubernetesClientProvider';
import { KubernetesFanOutHandler } from './KubernetesFanOutHandler';
@@ -99,6 +100,21 @@ export async function createRouter(
logger.info('Initializing Kubernetes backend');
const customResources: CustomResource[] = (
options.config.getOptionalConfigArray('kubernetes.customResources') ?? []
).map(
c =>
({
group: c.getString('group'),
apiVersion: c.getString('apiVersion'),
plural: c.getString('plural'),
} as CustomResource),
);
logger.info(
`action=LoadingCustomResources numOfCustomResources=${customResources.length}`,
);
const fetcher = new KubernetesClientBasedFetcher({
kubernetesClientProvider: new KubernetesClientProvider(),
logger,
@@ -122,6 +138,7 @@ export async function createRouter(
logger,
fetcher,
serviceLocator,
customResources,
);
return makeRouter(logger, kubernetesFanOutHandler, clusterDetails);
+16 -2
View File
@@ -61,7 +61,8 @@ export type FetchResponse =
| DeploymentFetchResponse
| ReplicaSetsFetchResponse
| HorizontalPodAutoscalersFetchResponse
| IngressesFetchResponse;
| IngressesFetchResponse
| CustomResourceFetchResponse;
// TODO fairly sure there's a easier way to do this
@@ -72,7 +73,8 @@ export type KubernetesObjectTypes =
| 'deployments'
| 'replicasets'
| 'horizontalpodautoscalers'
| 'ingresses';
| 'ingresses'
| 'customresources';
export interface PodFetchResponse {
type: 'pods';
@@ -109,11 +111,17 @@ export interface IngressesFetchResponse {
resources: Array<ExtensionsV1beta1Ingress>;
}
export interface CustomResourceFetchResponse {
type: 'customresources';
resources: Array<any>;
}
export interface ObjectFetchParams {
serviceId: string;
clusterDetails: ClusterDetails;
objectTypesToFetch: Set<KubernetesObjectTypes>;
labelSelector: string;
customResources: CustomResource[];
}
// Fetches information from a kubernetes cluster using the cluster details object
@@ -192,3 +200,9 @@ export type ClusterLocatorMethod =
export type ServiceLocatorMethod = 'multiTenant' | 'http'; // TODO implement http
export type AuthProviderType = 'google' | 'serviceAccount' | 'aws';
export interface CustomResource {
group: string;
apiVersion: string;
plural: string;
}