K8s add cluster lookup (#2571)
* Kubernetes plugins boilerplate * pr feedback; check for k8s annotation * temp * fix config * add example local setup * dice rolelr * rever yarn lock update * forgotten prettier * prettier again * master lock * update lock with k8s deps * prettier * router tests * internal error * fix package.json * PR feedback * more pr feedback * rename component file * kubernetes fetcher tests * fix test * option constructor
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
# Dice roller
|
||||
|
||||
An app to roll dice (it doesn't actually do that).
|
||||
|
||||
# Viewing in local Minikube running Backstage locally
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- kubectl installed
|
||||
- Minikube installed
|
||||
- jq installed
|
||||
- Backstage locally built and ready to run
|
||||
|
||||
## Steps
|
||||
|
||||
1. Start minikube
|
||||
2. Get the Kubernetes master base url `kubectl cluster-info`
|
||||
3. Apply manifests `kubectl apply -f dice-roller-manifests.yaml`
|
||||
4. Get service account token (see below)
|
||||
5. Start Backstage UI and backend
|
||||
6. Register existing component in Backstage
|
||||
- https://github.com/mclarke47/dice-roller/blob/master/catalog-info.yaml
|
||||
|
||||
Update `app-config.yaml` as follows.
|
||||
|
||||
```yaml
|
||||
---
|
||||
kubernetes:
|
||||
clusterLocatorMethod: 'configMultiTenant'
|
||||
clusters:
|
||||
- url: <KUBERNETES MASTER BASE URL FROM STEP 2>
|
||||
name: minikube
|
||||
serviceAccountToken: <TOKEN FROM STEP 4>
|
||||
```
|
||||
|
||||
### Getting the service account token
|
||||
|
||||
```
|
||||
kubectl get secret DICE_ROLLER_TOKEN_NAME -o=json | jq -r '.data["token"]' | base64 --decode | pbcopy
|
||||
```
|
||||
|
||||
Paste into `app-config.yaml` `kubernetes.clusters[].serviceAccountToken`
|
||||
@@ -0,0 +1,13 @@
|
||||
apiVersion: backstage.io/v1alpha1
|
||||
kind: Component
|
||||
metadata:
|
||||
name: dice-roller
|
||||
description: It rolls dice
|
||||
tags:
|
||||
- go
|
||||
annotations:
|
||||
'backstage.io/kubernetes-id': dice-roller
|
||||
spec:
|
||||
type: service
|
||||
lifecycle: production
|
||||
owner: guest
|
||||
@@ -0,0 +1,79 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: dice-roller
|
||||
labels:
|
||||
'backstage.io/kubernetes-id': dice-roller
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app: dice-roller
|
||||
replicas: 2
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: dice-roller
|
||||
'backstage.io/kubernetes-id': dice-roller
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx
|
||||
image: nginx:1.14.2
|
||||
ports:
|
||||
- containerPort: 80
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: dice-roller
|
||||
namespace: default
|
||||
labels:
|
||||
'backstage.io/kubernetes-id': dice-roller
|
||||
data:
|
||||
foo: bar
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: dice-roller
|
||||
labels:
|
||||
'backstage.io/kubernetes-id': dice-roller
|
||||
type: Opaque
|
||||
data:
|
||||
username: YWRtaW4=
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: dice-roller
|
||||
labels:
|
||||
'backstage.io/kubernetes-id': dice-roller
|
||||
spec:
|
||||
selector:
|
||||
app: dice-roller
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 80
|
||||
targetPort: 9376
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: dice-roller
|
||||
automountServiceAccountToken: false
|
||||
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: sa-admin
|
||||
roleRef:
|
||||
kind: ClusterRole
|
||||
name: cluster-admin
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: dice-roller
|
||||
namespace: default
|
||||
@@ -21,7 +21,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/backend-common": "^0.1.1-alpha.23",
|
||||
"@backstage/config": "^0.1.1-alpha.23",
|
||||
"@types/express": "^4.17.6",
|
||||
"@kubernetes/client-node": "^0.12.1",
|
||||
"compression": "^1.7.4",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.17.1",
|
||||
@@ -29,12 +31,14 @@
|
||||
"fs-extra": "^9.0.0",
|
||||
"helmet": "^4.0.0",
|
||||
"morgan": "^1.10.0",
|
||||
"stream-buffers": "^3.0.2",
|
||||
"winston": "^3.2.1",
|
||||
"yn": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.23",
|
||||
"jest-fetch-mock": "^3.0.3"
|
||||
"jest-fetch-mock": "^3.0.3",
|
||||
"supertest": "^4.0.2",
|
||||
"@backstage/cli": "^0.1.1-alpha.23"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import '@backstage/backend-common';
|
||||
import { MultiTenantConfigClusterLocator } from './MultiTenantConfigClusterLocator';
|
||||
import { ConfigReader, Config } from '@backstage/config';
|
||||
|
||||
describe('MultiTenantConfigClusterLocator', () => {
|
||||
it('empty clusters returns empty cluster details', async () => {
|
||||
const config: Config = new ConfigReader(
|
||||
{
|
||||
clusters: [],
|
||||
},
|
||||
'ctx',
|
||||
);
|
||||
|
||||
const sut = MultiTenantConfigClusterLocator.fromConfig(
|
||||
config.getConfigArray('clusters'),
|
||||
);
|
||||
|
||||
const result = await sut.getClusterByServiceId('ignored');
|
||||
|
||||
expect(result).toStrictEqual([]);
|
||||
});
|
||||
|
||||
it('one clusters returns one cluster details', async () => {
|
||||
const config: Config = new ConfigReader(
|
||||
{
|
||||
clusters: [
|
||||
{
|
||||
name: 'cluster1',
|
||||
url: 'http://localhost:8080',
|
||||
},
|
||||
],
|
||||
},
|
||||
'ctx',
|
||||
);
|
||||
|
||||
const sut = MultiTenantConfigClusterLocator.fromConfig(
|
||||
config.getConfigArray('clusters'),
|
||||
);
|
||||
|
||||
const result = await sut.getClusterByServiceId('ignored');
|
||||
|
||||
expect(result).toStrictEqual([
|
||||
{
|
||||
name: 'cluster1',
|
||||
serviceAccountToken: undefined,
|
||||
url: 'http://localhost:8080',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('two clusters returns two cluster details', async () => {
|
||||
const config: Config = new ConfigReader(
|
||||
{
|
||||
clusters: [
|
||||
{
|
||||
name: 'cluster1',
|
||||
serviceAccountToken: undefined,
|
||||
url: 'http://localhost:8080',
|
||||
},
|
||||
{
|
||||
name: 'cluster2',
|
||||
serviceAccountToken: undefined,
|
||||
url: 'http://localhost:8081',
|
||||
},
|
||||
],
|
||||
},
|
||||
'ctx',
|
||||
);
|
||||
|
||||
const sut = MultiTenantConfigClusterLocator.fromConfig(
|
||||
config.getConfigArray('clusters'),
|
||||
);
|
||||
|
||||
const result = await sut.getClusterByServiceId('ignored');
|
||||
|
||||
expect(result).toStrictEqual([
|
||||
{
|
||||
name: 'cluster1',
|
||||
serviceAccountToken: undefined,
|
||||
url: 'http://localhost:8080',
|
||||
},
|
||||
{
|
||||
name: 'cluster2',
|
||||
serviceAccountToken: undefined,
|
||||
url: 'http://localhost:8081',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Config } from '@backstage/config';
|
||||
import { ClusterDetails, KubernetesClusterLocator } from '..';
|
||||
|
||||
// This cluster locator assumes that every service is located on every cluster
|
||||
// Therefore it will always return all clusters in an app configuration file
|
||||
export class MultiTenantConfigClusterLocator
|
||||
implements KubernetesClusterLocator {
|
||||
private readonly clusterDetails: ClusterDetails[];
|
||||
|
||||
constructor(clusterDetails: ClusterDetails[]) {
|
||||
this.clusterDetails = clusterDetails;
|
||||
}
|
||||
|
||||
static fromConfig(config: Config[]): MultiTenantConfigClusterLocator {
|
||||
return new MultiTenantConfigClusterLocator(
|
||||
config.map(c => {
|
||||
return {
|
||||
name: c.getString('name'),
|
||||
url: c.getString('url'),
|
||||
serviceAccountToken: c.getOptionalString('serviceAccountToken'),
|
||||
};
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// As this implementation always returns all clusters serviceId is ignored here
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async getClusterByServiceId(_serviceId: string): Promise<ClusterDetails[]> {
|
||||
return this.clusterDetails;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export type ClusterLocatorMethod = 'configMultiTenant' | 'http';
|
||||
@@ -15,3 +15,4 @@
|
||||
*/
|
||||
|
||||
export * from './service/router';
|
||||
export * from './types/types';
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import '@backstage/backend-common';
|
||||
import { KubernetesClientProvider } from './KubernetesClientProvider';
|
||||
|
||||
describe('KubernetesClientProvider', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('can get core client by cluster details', async () => {
|
||||
const sut = new KubernetesClientProvider();
|
||||
|
||||
const mockGetKubeConfig = jest.fn(sut.getKubeConfig.bind({}));
|
||||
|
||||
sut.getKubeConfig = mockGetKubeConfig;
|
||||
|
||||
const result = sut.getCoreClientByClusterDetails({
|
||||
name: 'cluster-name',
|
||||
url: 'http://localhost:9999',
|
||||
serviceAccountToken: 'TOKEN',
|
||||
});
|
||||
|
||||
expect(result.basePath).toBe('http://localhost:9999');
|
||||
// These fields aren't on the type but are there
|
||||
const auth = (result as any).authentications.default;
|
||||
expect(auth.users[0].token).toBe('TOKEN');
|
||||
expect(auth.clusters[0].name).toBe('cluster-name');
|
||||
|
||||
expect(mockGetKubeConfig.mock.calls.length).toBe(1);
|
||||
});
|
||||
|
||||
it('can get cached core client by cluster details', async () => {
|
||||
const sut = new KubernetesClientProvider();
|
||||
|
||||
const mockGetKubeConfig = jest.fn(sut.getKubeConfig.bind({}));
|
||||
|
||||
sut.getKubeConfig = mockGetKubeConfig;
|
||||
|
||||
const result1 = sut.getCoreClientByClusterDetails({
|
||||
name: 'cluster-name',
|
||||
url: 'http://localhost:9999',
|
||||
serviceAccountToken: 'TOKEN',
|
||||
});
|
||||
|
||||
const result2 = sut.getCoreClientByClusterDetails({
|
||||
name: 'cluster-name',
|
||||
url: 'http://localhost:9999',
|
||||
serviceAccountToken: 'TOKEN',
|
||||
});
|
||||
|
||||
expect(result1.basePath).toBe('http://localhost:9999');
|
||||
// These fields aren't on the type but are there
|
||||
const auth1 = (result1 as any).authentications.default;
|
||||
expect(auth1.users[0].token).toBe('TOKEN');
|
||||
expect(auth1.clusters[0].name).toBe('cluster-name');
|
||||
|
||||
expect(result2.basePath).toBe('http://localhost:9999');
|
||||
// These fields aren't on the type but are there
|
||||
const auth2 = (result2 as any).authentications.default;
|
||||
expect(auth2.users[0].token).toBe('TOKEN');
|
||||
expect(auth2.clusters[0].name).toBe('cluster-name');
|
||||
|
||||
expect(mockGetKubeConfig.mock.calls.length).toBe(1);
|
||||
});
|
||||
|
||||
it('can get apps client by cluster details', async () => {
|
||||
const sut = new KubernetesClientProvider();
|
||||
|
||||
const mockGetKubeConfig = jest.fn(sut.getKubeConfig.bind({}));
|
||||
|
||||
sut.getKubeConfig = mockGetKubeConfig;
|
||||
|
||||
const result = sut.getAppsClientByClusterDetails({
|
||||
name: 'cluster-name',
|
||||
url: 'http://localhost:9999',
|
||||
serviceAccountToken: 'TOKEN',
|
||||
});
|
||||
|
||||
expect(result.basePath).toBe('http://localhost:9999');
|
||||
// These fields aren't on the type but are there
|
||||
const auth = (result as any).authentications.default;
|
||||
expect(auth.users[0].token).toBe('TOKEN');
|
||||
expect(auth.clusters[0].name).toBe('cluster-name');
|
||||
|
||||
expect(mockGetKubeConfig.mock.calls.length).toBe(1);
|
||||
});
|
||||
|
||||
it('can get cached apps client by cluster details', async () => {
|
||||
const sut = new KubernetesClientProvider();
|
||||
|
||||
const mockGetKubeConfig = jest.fn(sut.getKubeConfig.bind({}));
|
||||
|
||||
sut.getKubeConfig = mockGetKubeConfig;
|
||||
|
||||
const result1 = sut.getAppsClientByClusterDetails({
|
||||
name: 'cluster-name',
|
||||
url: 'http://localhost:9999',
|
||||
serviceAccountToken: 'TOKEN',
|
||||
});
|
||||
|
||||
const result2 = sut.getAppsClientByClusterDetails({
|
||||
name: 'cluster-name',
|
||||
url: 'http://localhost:9999',
|
||||
serviceAccountToken: 'TOKEN',
|
||||
});
|
||||
|
||||
expect(result1.basePath).toBe('http://localhost:9999');
|
||||
// These fields aren't on the type but are there
|
||||
const auth1 = (result1 as any).authentications.default;
|
||||
expect(auth1.users[0].token).toBe('TOKEN');
|
||||
expect(auth1.clusters[0].name).toBe('cluster-name');
|
||||
|
||||
expect(result2.basePath).toBe('http://localhost:9999');
|
||||
// These fields aren't on the type but are there
|
||||
const auth2 = (result2 as any).authentications.default;
|
||||
expect(auth2.users[0].token).toBe('TOKEN');
|
||||
expect(auth2.clusters[0].name).toBe('cluster-name');
|
||||
|
||||
expect(mockGetKubeConfig.mock.calls.length).toBe(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ClusterDetails } from '..';
|
||||
import { AppsV1Api, CoreV1Api, KubeConfig } from '@kubernetes/client-node';
|
||||
|
||||
export class KubernetesClientProvider {
|
||||
private readonly coreClientMap: {
|
||||
[key: string]: CoreV1Api;
|
||||
};
|
||||
|
||||
private readonly appsClientMap: {
|
||||
[key: string]: AppsV1Api;
|
||||
};
|
||||
|
||||
constructor() {
|
||||
this.coreClientMap = {};
|
||||
this.appsClientMap = {};
|
||||
}
|
||||
|
||||
// visible for testing
|
||||
getKubeConfig(clusterDetails: ClusterDetails) {
|
||||
const cluster = {
|
||||
name: clusterDetails.name,
|
||||
server: clusterDetails.url,
|
||||
// TODO configure this
|
||||
skipTLSVerify: true,
|
||||
};
|
||||
|
||||
// TODO configure
|
||||
const user = {
|
||||
name: 'service-account',
|
||||
token: clusterDetails.serviceAccountToken,
|
||||
};
|
||||
|
||||
const context = {
|
||||
name: `${clusterDetails.name}`,
|
||||
user: user.name,
|
||||
cluster: cluster.name,
|
||||
};
|
||||
|
||||
const kc = new KubeConfig();
|
||||
kc.loadFromOptions({
|
||||
clusters: [cluster],
|
||||
users: [user],
|
||||
contexts: [context],
|
||||
currentContext: context.name,
|
||||
});
|
||||
return kc;
|
||||
}
|
||||
|
||||
getCoreClientByClusterDetails(clusterDetails: ClusterDetails) {
|
||||
const clientMapKey = clusterDetails.name;
|
||||
|
||||
if (this.coreClientMap.hasOwnProperty(clientMapKey)) {
|
||||
return this.coreClientMap[clientMapKey];
|
||||
}
|
||||
|
||||
const kc = this.getKubeConfig(clusterDetails);
|
||||
|
||||
const client = kc.makeApiClient(CoreV1Api);
|
||||
|
||||
this.coreClientMap[clientMapKey] = client;
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
getAppsClientByClusterDetails(clusterDetails: ClusterDetails) {
|
||||
const clientMapKey = clusterDetails.name;
|
||||
|
||||
if (this.appsClientMap.hasOwnProperty(clientMapKey)) {
|
||||
return this.appsClientMap[clientMapKey];
|
||||
}
|
||||
|
||||
const kc = this.getKubeConfig(clusterDetails);
|
||||
|
||||
const client = kc.makeApiClient(AppsV1Api);
|
||||
|
||||
this.appsClientMap[clientMapKey] = client;
|
||||
|
||||
return client;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { getVoidLogger } from '@backstage/backend-common';
|
||||
import { KubernetesClientBasedFetcher } from './KubernetesFetcher';
|
||||
|
||||
describe('KubernetesClientProvider', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should return pods, services', async () => {
|
||||
const clientMock: any = {
|
||||
listPodForAllNamespaces: jest.fn(),
|
||||
listServiceForAllNamespaces: jest.fn(),
|
||||
};
|
||||
|
||||
const kubernetesClientProvider: any = {
|
||||
getCoreClientByClusterDetails: jest.fn(() => clientMock),
|
||||
getAppsClientByClusterDetails: jest.fn(() => clientMock),
|
||||
};
|
||||
|
||||
const sut = new KubernetesClientBasedFetcher({
|
||||
kubernetesClientProvider,
|
||||
logger: getVoidLogger(),
|
||||
});
|
||||
|
||||
clientMock.listPodForAllNamespaces.mockResolvedValueOnce({
|
||||
body: {
|
||||
items: [
|
||||
{
|
||||
metadata: {
|
||||
name: 'pod-name',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
clientMock.listServiceForAllNamespaces.mockResolvedValueOnce({
|
||||
body: {
|
||||
items: [
|
||||
{
|
||||
metadata: {
|
||||
name: 'service-name',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const result = await sut.fetchObjectsByServiceId(
|
||||
'some-service',
|
||||
{
|
||||
name: 'cluster1',
|
||||
url: 'http://localhost:9999',
|
||||
serviceAccountToken: undefined,
|
||||
},
|
||||
new Set(['pods', 'services']),
|
||||
);
|
||||
|
||||
expect(result).toStrictEqual([
|
||||
{
|
||||
type: 'pods',
|
||||
resources: [
|
||||
{
|
||||
metadata: {
|
||||
name: 'pod-name',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'services',
|
||||
resources: [
|
||||
{
|
||||
metadata: {
|
||||
name: 'service-name',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
expect(clientMock.listPodForAllNamespaces.mock.calls.length).toBe(1);
|
||||
expect(clientMock.listServiceForAllNamespaces.mock.calls.length).toBe(1);
|
||||
|
||||
expect(
|
||||
kubernetesClientProvider.getAppsClientByClusterDetails.mock.calls.length,
|
||||
).toBe(2);
|
||||
expect(
|
||||
kubernetesClientProvider.getCoreClientByClusterDetails.mock.calls.length,
|
||||
).toBe(2);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,212 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
AppsV1Api,
|
||||
CoreV1Api,
|
||||
V1ConfigMap,
|
||||
V1Deployment,
|
||||
V1Pod,
|
||||
V1ReplicaSet,
|
||||
V1Secret,
|
||||
} from '@kubernetes/client-node';
|
||||
import { KubernetesClientProvider } from './KubernetesClientProvider';
|
||||
import { V1Service } from '@kubernetes/client-node/dist/gen/model/v1Service';
|
||||
import { Logger } from 'winston';
|
||||
import {
|
||||
KubernetesFetcher,
|
||||
ClusterDetails,
|
||||
KubernetesObjectTypes,
|
||||
FetchResponse,
|
||||
} from '..';
|
||||
|
||||
export interface Clients {
|
||||
core: CoreV1Api;
|
||||
apps: AppsV1Api;
|
||||
}
|
||||
|
||||
export interface KubernetesClientBasedFetcherOptions {
|
||||
kubernetesClientProvider: KubernetesClientProvider;
|
||||
logger: Logger;
|
||||
}
|
||||
|
||||
export class KubernetesClientBasedFetcher implements KubernetesFetcher {
|
||||
private readonly kubernetesClientProvider: KubernetesClientProvider;
|
||||
private readonly logger: Logger;
|
||||
|
||||
constructor({
|
||||
kubernetesClientProvider,
|
||||
logger,
|
||||
}: KubernetesClientBasedFetcherOptions) {
|
||||
this.kubernetesClientProvider = kubernetesClientProvider;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
fetchObjectsByServiceId(
|
||||
serviceId: string,
|
||||
clusterDetails: ClusterDetails,
|
||||
objectTypesToFetch: Set<KubernetesObjectTypes>,
|
||||
): Promise<FetchResponse[]> {
|
||||
return Promise.all(
|
||||
Array.from(objectTypesToFetch).map(type => {
|
||||
return this.fetchByObjectType(serviceId, clusterDetails, type);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private fetchByObjectType(
|
||||
serviceId: string,
|
||||
clusterDetails: ClusterDetails,
|
||||
type: KubernetesObjectTypes,
|
||||
): Promise<FetchResponse> {
|
||||
switch (type) {
|
||||
case 'pods':
|
||||
return this.fetchPodsByServiceId(serviceId, clusterDetails).then(r => ({
|
||||
type: type,
|
||||
resources: r,
|
||||
}));
|
||||
case 'configmaps':
|
||||
return this.fetchConfigMapsByServiceId(
|
||||
serviceId,
|
||||
clusterDetails,
|
||||
).then(r => ({ type: type, resources: r }));
|
||||
case 'deployments':
|
||||
return this.fetchDeploymentsByServiceId(
|
||||
serviceId,
|
||||
clusterDetails,
|
||||
).then(r => ({ type: type, resources: r }));
|
||||
case 'replicasets':
|
||||
return this.fetchReplicaSetsByServiceId(
|
||||
serviceId,
|
||||
clusterDetails,
|
||||
).then(r => ({ type: type, resources: r }));
|
||||
case 'secrets':
|
||||
return this.fetchSecretsByServiceId(
|
||||
serviceId,
|
||||
clusterDetails,
|
||||
).then(r => ({ type: type, resources: r }));
|
||||
case 'services':
|
||||
return this.fetchServicesByServiceId(
|
||||
serviceId,
|
||||
clusterDetails,
|
||||
).then(r => ({ type: type, resources: r }));
|
||||
default:
|
||||
// unrecognised type
|
||||
throw new Error(`unrecognised type=${type}`);
|
||||
}
|
||||
}
|
||||
|
||||
private singleClusterFetch<T>(
|
||||
clusterDetails: ClusterDetails,
|
||||
fn: (client: Clients) => Promise<{ body: { items: Array<T> } }>,
|
||||
): Promise<Array<T>> {
|
||||
const core = this.kubernetesClientProvider.getCoreClientByClusterDetails(
|
||||
clusterDetails,
|
||||
);
|
||||
const apps = this.kubernetesClientProvider.getAppsClientByClusterDetails(
|
||||
clusterDetails,
|
||||
);
|
||||
|
||||
this.logger.debug(`calling cluster=${clusterDetails.name}`);
|
||||
return fn({ core, apps }).then(result => {
|
||||
return result.body.items;
|
||||
});
|
||||
}
|
||||
|
||||
private fetchServicesByServiceId(
|
||||
serviceId: string,
|
||||
clusterDetails: ClusterDetails,
|
||||
): Promise<Array<V1Service>> {
|
||||
return this.singleClusterFetch<V1Service>(clusterDetails, ({ core }) =>
|
||||
core.listServiceForAllNamespaces(
|
||||
false,
|
||||
'',
|
||||
'',
|
||||
`backstage.io/kubernetes-id=${serviceId}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
private fetchPodsByServiceId(
|
||||
serviceId: string,
|
||||
clusterDetails: ClusterDetails,
|
||||
): Promise<Array<V1Pod>> {
|
||||
return this.singleClusterFetch<V1Pod>(clusterDetails, ({ core }) =>
|
||||
core.listPodForAllNamespaces(
|
||||
false,
|
||||
'',
|
||||
'',
|
||||
`backstage.io/kubernetes-id=${serviceId}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
private fetchConfigMapsByServiceId(
|
||||
serviceId: string,
|
||||
clusterDetails: ClusterDetails,
|
||||
): Promise<Array<V1ConfigMap>> {
|
||||
return this.singleClusterFetch<V1Pod>(clusterDetails, ({ core }) =>
|
||||
core.listConfigMapForAllNamespaces(
|
||||
false,
|
||||
'',
|
||||
'',
|
||||
`backstage.io/kubernetes-id=${serviceId}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
private fetchSecretsByServiceId(
|
||||
serviceId: string,
|
||||
clusterDetails: ClusterDetails,
|
||||
): Promise<Array<V1Secret>> {
|
||||
return this.singleClusterFetch<V1Secret>(clusterDetails, ({ core }) =>
|
||||
core.listSecretForAllNamespaces(
|
||||
false,
|
||||
'',
|
||||
'',
|
||||
`backstage.io/kubernetes-id=${serviceId}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
private fetchDeploymentsByServiceId(
|
||||
serviceId: string,
|
||||
clusterDetails: ClusterDetails,
|
||||
): Promise<Array<V1Deployment>> {
|
||||
return this.singleClusterFetch<V1Deployment>(clusterDetails, ({ apps }) =>
|
||||
apps.listDeploymentForAllNamespaces(
|
||||
false,
|
||||
'',
|
||||
'',
|
||||
`backstage.io/kubernetes-id=${serviceId}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
private fetchReplicaSetsByServiceId(
|
||||
serviceId: string,
|
||||
clusterDetails: ClusterDetails,
|
||||
): Promise<Array<V1ReplicaSet>> {
|
||||
return this.singleClusterFetch<V1ReplicaSet>(clusterDetails, ({ apps }) =>
|
||||
apps.listReplicaSetForAllNamespaces(
|
||||
false,
|
||||
'',
|
||||
'',
|
||||
`backstage.io/kubernetes-id=${serviceId}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+242
@@ -0,0 +1,242 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { handleGetKubernetesObjectsByServiceId } from './getKubernetesObjectsByServiceIdHandler';
|
||||
import { getVoidLogger } from '@backstage/backend-common';
|
||||
import { ClusterDetails } from '..';
|
||||
|
||||
const TEST_SERVICE_ID = 'my-service';
|
||||
|
||||
const fetchObjectsByServiceId = jest.fn();
|
||||
|
||||
const getClusterByServiceId = jest.fn();
|
||||
|
||||
const mockFetch = (mock: jest.Mock) => {
|
||||
mock.mockImplementation((serviceId: string, clusterDetails: ClusterDetails) =>
|
||||
Promise.resolve([
|
||||
{
|
||||
type: 'pods',
|
||||
resources: [
|
||||
{
|
||||
metadata: {
|
||||
name: `my-pods-${serviceId}-${clusterDetails.name}`,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'configmaps',
|
||||
resources: [
|
||||
{
|
||||
metadata: {
|
||||
name: `my-configmaps-${serviceId}-${clusterDetails.name}`,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'services',
|
||||
resources: [
|
||||
{
|
||||
metadata: {
|
||||
name: `my-services-${serviceId}-${clusterDetails.name}`,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
]),
|
||||
);
|
||||
};
|
||||
|
||||
describe('handleGetKubernetesObjectsByServiceId', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('retrieve objects for one cluster', async () => {
|
||||
getClusterByServiceId.mockImplementation(() =>
|
||||
Promise.resolve([
|
||||
{
|
||||
name: 'test-cluster',
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
mockFetch(fetchObjectsByServiceId);
|
||||
|
||||
const result = await handleGetKubernetesObjectsByServiceId(
|
||||
TEST_SERVICE_ID,
|
||||
{
|
||||
fetchObjectsByServiceId,
|
||||
},
|
||||
{
|
||||
getClusterByServiceId,
|
||||
},
|
||||
getVoidLogger(),
|
||||
);
|
||||
|
||||
expect(getClusterByServiceId.mock.calls.length).toBe(1);
|
||||
expect(fetchObjectsByServiceId.mock.calls.length).toBe(1);
|
||||
expect(result).toStrictEqual({
|
||||
items: [
|
||||
{
|
||||
cluster: {
|
||||
name: 'test-cluster',
|
||||
},
|
||||
resources: [
|
||||
{
|
||||
resources: [
|
||||
{
|
||||
metadata: {
|
||||
name: 'my-pods-my-service-test-cluster',
|
||||
},
|
||||
},
|
||||
],
|
||||
type: 'pods',
|
||||
},
|
||||
{
|
||||
resources: [
|
||||
{
|
||||
metadata: {
|
||||
name: 'my-configmaps-my-service-test-cluster',
|
||||
},
|
||||
},
|
||||
],
|
||||
type: 'configmaps',
|
||||
},
|
||||
{
|
||||
resources: [
|
||||
{
|
||||
metadata: {
|
||||
name: 'my-services-my-service-test-cluster',
|
||||
},
|
||||
},
|
||||
],
|
||||
type: 'services',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('retrieve objects for two clusters', async () => {
|
||||
getClusterByServiceId.mockImplementation(() =>
|
||||
Promise.resolve([
|
||||
{
|
||||
name: 'test-cluster',
|
||||
},
|
||||
{
|
||||
name: 'other-cluster',
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
mockFetch(fetchObjectsByServiceId);
|
||||
|
||||
const result = await handleGetKubernetesObjectsByServiceId(
|
||||
TEST_SERVICE_ID,
|
||||
{
|
||||
fetchObjectsByServiceId,
|
||||
},
|
||||
{
|
||||
getClusterByServiceId,
|
||||
},
|
||||
getVoidLogger(),
|
||||
);
|
||||
|
||||
expect(getClusterByServiceId.mock.calls.length).toBe(1);
|
||||
expect(fetchObjectsByServiceId.mock.calls.length).toBe(2);
|
||||
expect(result).toStrictEqual({
|
||||
items: [
|
||||
{
|
||||
cluster: {
|
||||
name: 'test-cluster',
|
||||
},
|
||||
resources: [
|
||||
{
|
||||
resources: [
|
||||
{
|
||||
metadata: {
|
||||
name: 'my-pods-my-service-test-cluster',
|
||||
},
|
||||
},
|
||||
],
|
||||
type: 'pods',
|
||||
},
|
||||
{
|
||||
resources: [
|
||||
{
|
||||
metadata: {
|
||||
name: 'my-configmaps-my-service-test-cluster',
|
||||
},
|
||||
},
|
||||
],
|
||||
type: 'configmaps',
|
||||
},
|
||||
{
|
||||
resources: [
|
||||
{
|
||||
metadata: {
|
||||
name: 'my-services-my-service-test-cluster',
|
||||
},
|
||||
},
|
||||
],
|
||||
type: 'services',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
cluster: {
|
||||
name: 'other-cluster',
|
||||
},
|
||||
resources: [
|
||||
{
|
||||
resources: [
|
||||
{
|
||||
metadata: {
|
||||
name: 'my-pods-my-service-other-cluster',
|
||||
},
|
||||
},
|
||||
],
|
||||
type: 'pods',
|
||||
},
|
||||
{
|
||||
resources: [
|
||||
{
|
||||
metadata: {
|
||||
name: 'my-configmaps-my-service-other-cluster',
|
||||
},
|
||||
},
|
||||
],
|
||||
type: 'configmaps',
|
||||
},
|
||||
{
|
||||
resources: [
|
||||
{
|
||||
metadata: {
|
||||
name: 'my-services-my-service-other-cluster',
|
||||
},
|
||||
},
|
||||
],
|
||||
type: 'services',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Logger } from 'winston';
|
||||
import {
|
||||
KubernetesClusterLocator,
|
||||
KubernetesFetcher,
|
||||
KubernetesObjectTypes,
|
||||
ObjectsByServiceIdResponse,
|
||||
} from '..';
|
||||
|
||||
export type GetKubernetesObjectsByServiceIdHandler = (
|
||||
serviceId: string,
|
||||
fetcher: KubernetesFetcher,
|
||||
clusterLocator: KubernetesClusterLocator,
|
||||
logger: Logger,
|
||||
objectsToFetch?: Set<KubernetesObjectTypes>,
|
||||
) => Promise<ObjectsByServiceIdResponse>;
|
||||
|
||||
const DEFAULT_OBJECTS = new Set<KubernetesObjectTypes>([
|
||||
'pods',
|
||||
'services',
|
||||
'configmaps',
|
||||
'secrets',
|
||||
'deployments',
|
||||
'replicasets',
|
||||
]);
|
||||
|
||||
export const handleGetKubernetesObjectsByServiceId: GetKubernetesObjectsByServiceIdHandler = async (
|
||||
serviceId,
|
||||
fetcher,
|
||||
clusterLocator,
|
||||
logger,
|
||||
objectsToFetch = DEFAULT_OBJECTS,
|
||||
) => {
|
||||
const clusterDetails = await clusterLocator.getClusterByServiceId(serviceId);
|
||||
|
||||
logger.info(
|
||||
`serviceId=${serviceId} clusterDetails=${clusterDetails.map(c => c.name)}`,
|
||||
);
|
||||
|
||||
return Promise.all(
|
||||
clusterDetails.map(cd => {
|
||||
return fetcher
|
||||
.fetchObjectsByServiceId(serviceId, cd, objectsToFetch)
|
||||
.then(result => {
|
||||
return {
|
||||
cluster: {
|
||||
name: cd.name,
|
||||
},
|
||||
resources: result,
|
||||
};
|
||||
});
|
||||
}),
|
||||
).then(r => ({ items: r }));
|
||||
};
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { getVoidLogger } from '@backstage/backend-common';
|
||||
import express from 'express';
|
||||
import request from 'supertest';
|
||||
import { makeRouter } from './router';
|
||||
import {
|
||||
KubernetesClusterLocator,
|
||||
KubernetesFetcher,
|
||||
ObjectsByServiceIdResponse,
|
||||
} from '..';
|
||||
|
||||
describe('router', () => {
|
||||
let app: express.Express;
|
||||
let kubernetesFetcher: jest.Mocked<KubernetesFetcher>;
|
||||
let kubernetesClusterLocator: jest.Mocked<KubernetesClusterLocator>;
|
||||
let handleGetByServiceId: jest.Mock<Promise<ObjectsByServiceIdResponse>>;
|
||||
|
||||
beforeAll(async () => {
|
||||
kubernetesFetcher = {
|
||||
fetchObjectsByServiceId: jest.fn(),
|
||||
};
|
||||
|
||||
kubernetesClusterLocator = {
|
||||
getClusterByServiceId: jest.fn(),
|
||||
};
|
||||
|
||||
handleGetByServiceId = jest.fn();
|
||||
|
||||
const router = makeRouter(
|
||||
getVoidLogger(),
|
||||
kubernetesFetcher,
|
||||
kubernetesClusterLocator,
|
||||
handleGetByServiceId as any,
|
||||
);
|
||||
app = express().use(router);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
describe('GET /services/:serviceId', () => {
|
||||
it('happy path: lists kubernetes objects', async () => {
|
||||
const result = {
|
||||
clusterOne: {
|
||||
pods: [
|
||||
{
|
||||
metadata: {
|
||||
name: 'pod1',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
} as any;
|
||||
handleGetByServiceId.mockReturnValueOnce(Promise.resolve(result));
|
||||
|
||||
const response = await request(app).get('/services/test-service');
|
||||
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.body).toEqual(result);
|
||||
});
|
||||
|
||||
it('internal error: lists kubernetes objects', async () => {
|
||||
handleGetByServiceId.mockRejectedValue(Error('some internal error'));
|
||||
|
||||
const response = await request(app).get('/services/test-service');
|
||||
|
||||
expect(response.status).toEqual(500);
|
||||
expect(response.body).toEqual({ error: 'some internal error' });
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -17,19 +17,65 @@
|
||||
import express from 'express';
|
||||
import Router from 'express-promise-router';
|
||||
import { Logger } from 'winston';
|
||||
import { Config } from '@backstage/config';
|
||||
import { ClusterLocatorMethod } from '../cluster-locator/types';
|
||||
import { MultiTenantConfigClusterLocator } from '../cluster-locator/MultiTenantConfigClusterLocator';
|
||||
import { KubernetesClientBasedFetcher } from './KubernetesFetcher';
|
||||
import { KubernetesClientProvider } from './KubernetesClientProvider';
|
||||
import {
|
||||
GetKubernetesObjectsByServiceIdHandler,
|
||||
handleGetKubernetesObjectsByServiceId,
|
||||
} from './getKubernetesObjectsByServiceIdHandler';
|
||||
import { KubernetesClusterLocator, KubernetesFetcher } from '..';
|
||||
|
||||
export interface RouterOptions {
|
||||
logger: Logger;
|
||||
config: Config;
|
||||
}
|
||||
|
||||
const makeRouter = (logger: Logger): express.Router => {
|
||||
const getClusterLocator = (config: Config): KubernetesClusterLocator => {
|
||||
const clusterLocatorMethod = config.getString(
|
||||
'kubernetes.clusterLocatorMethod',
|
||||
) as ClusterLocatorMethod;
|
||||
|
||||
switch (clusterLocatorMethod) {
|
||||
case 'configMultiTenant':
|
||||
return MultiTenantConfigClusterLocator.fromConfig(
|
||||
config.getConfigArray('kubernetes.clusters'),
|
||||
);
|
||||
case 'http':
|
||||
throw new Error('not implemented');
|
||||
default:
|
||||
throw new Error(
|
||||
`Unsupported kubernetes.clusterLocatorMethod "${clusterLocatorMethod}"`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const makeRouter = (
|
||||
logger: Logger,
|
||||
fetcher: KubernetesFetcher,
|
||||
clusterLocator: KubernetesClusterLocator,
|
||||
handleGetByServiceId: GetKubernetesObjectsByServiceIdHandler,
|
||||
): express.Router => {
|
||||
const router = Router();
|
||||
router.use(express.json());
|
||||
|
||||
// TODO error handling
|
||||
router.get('/services/:serviceId', async (req, res) => {
|
||||
const serviceId = req.params.serviceId;
|
||||
logger.info(`HERE ${serviceId}`);
|
||||
res.send({ serviceId });
|
||||
|
||||
try {
|
||||
const response = await handleGetByServiceId(
|
||||
serviceId,
|
||||
fetcher,
|
||||
clusterLocator,
|
||||
logger,
|
||||
);
|
||||
res.send(response);
|
||||
} catch (e) {
|
||||
res.status(500).send({ error: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
return router;
|
||||
@@ -41,5 +87,18 @@ export async function createRouter(
|
||||
const logger = options.logger;
|
||||
|
||||
logger.info('Initializing Kubernetes backend');
|
||||
return makeRouter(logger);
|
||||
|
||||
const clusterLocator = getClusterLocator(options.config);
|
||||
|
||||
const fetcher = new KubernetesClientBasedFetcher({
|
||||
kubernetesClientProvider: new KubernetesClientProvider(),
|
||||
logger,
|
||||
});
|
||||
|
||||
return makeRouter(
|
||||
logger,
|
||||
fetcher,
|
||||
clusterLocator,
|
||||
handleGetKubernetesObjectsByServiceId,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import express from 'express';
|
||||
import helmet from 'helmet';
|
||||
import { Logger } from 'winston';
|
||||
import { createRouter } from './router';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
|
||||
export interface ApplicationOptions {
|
||||
enableCors: boolean;
|
||||
@@ -35,6 +36,7 @@ export async function createStandaloneApplication(
|
||||
options: ApplicationOptions,
|
||||
): Promise<express.Application> {
|
||||
const { enableCors, logger } = options;
|
||||
const config = ConfigReader.fromConfigs([]);
|
||||
const app = express();
|
||||
|
||||
app.use(helmet());
|
||||
@@ -44,7 +46,7 @@ export async function createStandaloneApplication(
|
||||
app.use(compression());
|
||||
app.use(express.json());
|
||||
app.use(requestLoggingHandler());
|
||||
app.use('/', await createRouter({ logger }));
|
||||
app.use('/', await createRouter({ logger, config }));
|
||||
app.use(notFoundHandler());
|
||||
app.use(errorHandler());
|
||||
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
V1ConfigMap,
|
||||
V1Deployment,
|
||||
V1Pod,
|
||||
V1ReplicaSet,
|
||||
V1Secret,
|
||||
V1Service,
|
||||
} from '@kubernetes/client-node';
|
||||
|
||||
export interface ClusterDetails {
|
||||
name: string;
|
||||
url: string;
|
||||
// TODO this will eventually be configured by the auth translation work
|
||||
serviceAccountToken: string | undefined;
|
||||
}
|
||||
|
||||
export interface ClusterObjects {
|
||||
cluster: { name: string };
|
||||
resources: FetchResponse[];
|
||||
}
|
||||
|
||||
export interface ObjectsByServiceIdResponse {
|
||||
items: ClusterObjects[];
|
||||
}
|
||||
|
||||
export type FetchResponse =
|
||||
| PodFetchResponse
|
||||
| ServiceFetchResponse
|
||||
| ConfigMapFetchResponse
|
||||
| SecretFetchResponse
|
||||
| DeploymentFetchResponse
|
||||
| ReplicaSetsFetchResponse;
|
||||
|
||||
// TODO fairly sure there's a easier way to do this
|
||||
|
||||
export type KubernetesObjectTypes =
|
||||
| 'pods'
|
||||
| 'services'
|
||||
| 'configmaps'
|
||||
| 'secrets'
|
||||
| 'deployments'
|
||||
| 'replicasets';
|
||||
|
||||
export interface PodFetchResponse {
|
||||
type: 'pods';
|
||||
resources: Array<V1Pod>;
|
||||
}
|
||||
|
||||
export interface ServiceFetchResponse {
|
||||
type: 'services';
|
||||
resources: Array<V1Service>;
|
||||
}
|
||||
|
||||
export interface ConfigMapFetchResponse {
|
||||
type: 'configmaps';
|
||||
resources: Array<V1ConfigMap>;
|
||||
}
|
||||
|
||||
export interface SecretFetchResponse {
|
||||
type: 'secrets';
|
||||
resources: Array<V1Secret>;
|
||||
}
|
||||
|
||||
export interface DeploymentFetchResponse {
|
||||
type: 'deployments';
|
||||
resources: Array<V1Deployment>;
|
||||
}
|
||||
|
||||
export interface ReplicaSetsFetchResponse {
|
||||
type: 'replicasets';
|
||||
resources: Array<V1ReplicaSet>;
|
||||
}
|
||||
|
||||
// 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>,
|
||||
): Promise<FetchResponse[]>;
|
||||
}
|
||||
|
||||
// Used to locate which cluster(s) a service is running on
|
||||
export interface KubernetesClusterLocator {
|
||||
getClusterByServiceId(serviceId: string): Promise<ClusterDetails[]>;
|
||||
}
|
||||
Reference in New Issue
Block a user