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:
+2
-1
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user