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:
@@ -55,6 +55,10 @@ newrelic:
|
||||
lighthouse:
|
||||
baseUrl: http://localhost:3003
|
||||
|
||||
kubernetes:
|
||||
clusterLocatorMethod: 'configMultiTenant'
|
||||
clusters: []
|
||||
|
||||
catalog:
|
||||
rules:
|
||||
- allow: [Component, API, Group, Template, Location]
|
||||
|
||||
@@ -17,6 +17,9 @@
|
||||
import { createRouter } from '@backstage/plugin-kubernetes-backend';
|
||||
import { PluginEnvironment } from '../types';
|
||||
|
||||
export default async function createPlugin({ logger }: PluginEnvironment) {
|
||||
return await createRouter({ logger });
|
||||
export default async function createPlugin({
|
||||
logger,
|
||||
config,
|
||||
}: PluginEnvironment) {
|
||||
return await createRouter({ logger, config });
|
||||
}
|
||||
|
||||
@@ -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[]>;
|
||||
}
|
||||
@@ -22,6 +22,7 @@
|
||||
"dependencies": {
|
||||
"@backstage/catalog-model": "^0.1.1-alpha.23",
|
||||
"@backstage/core": "^0.1.1-alpha.23",
|
||||
"@backstage/plugin-kubernetes-backend": "^0.1.1-alpha.23",
|
||||
"@backstage/theme": "^0.1.1-alpha.23",
|
||||
"@material-ui/core": "^4.11.0",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
|
||||
@@ -22,7 +22,7 @@ import { rootCatalogKubernetesRouteRef } from './plugin';
|
||||
import { KubernetesContent } from './components/KubernetesContent';
|
||||
import { WarningPanel } from '@backstage/core';
|
||||
|
||||
const KUBERNETES_ANNOTATION = 'backstage.io/kubernetes';
|
||||
const KUBERNETES_ANNOTATION = 'backstage.io/kubernetes-id';
|
||||
|
||||
export const Router = ({ entity }: { entity: Entity }) => {
|
||||
const kubernetesAnnotationValue =
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
import { DiscoveryApi } from '@backstage/core';
|
||||
import { KubernetesApi } from './types';
|
||||
import { ObjectsByServiceIdResponse } from '@backstage/plugin-kubernetes-backend';
|
||||
|
||||
export class KubernetesBackendClient implements KubernetesApi {
|
||||
private readonly discoveryApi: DiscoveryApi;
|
||||
@@ -37,7 +38,9 @@ export class KubernetesBackendClient implements KubernetesApi {
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
async getObjectsByServiceId(serviceId: String): Promise<{}> {
|
||||
async getObjectsByServiceId(
|
||||
serviceId: String,
|
||||
): Promise<ObjectsByServiceIdResponse> {
|
||||
return await this.getRequired(`/services/${serviceId}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
|
||||
import { createApiRef } from '@backstage/core';
|
||||
import { ObjectsByServiceIdResponse } from '@backstage/plugin-kubernetes-backend';
|
||||
|
||||
export const kubernetesApiRef = createApiRef<KubernetesApi>({
|
||||
id: 'plugin.kubernetes.service',
|
||||
@@ -23,5 +24,5 @@ export const kubernetesApiRef = createApiRef<KubernetesApi>({
|
||||
});
|
||||
|
||||
export interface KubernetesApi {
|
||||
getObjectsByServiceId(serviceId: String): Promise<{}>;
|
||||
getObjectsByServiceId(serviceId: String): Promise<ObjectsByServiceIdResponse>;
|
||||
}
|
||||
|
||||
@@ -15,25 +15,19 @@
|
||||
*/
|
||||
|
||||
import React, { FC, useEffect, useState } from 'react';
|
||||
import { Typography, Grid } from '@material-ui/core';
|
||||
import {
|
||||
InfoCard,
|
||||
Page,
|
||||
pageTheme,
|
||||
Content,
|
||||
ContentHeader,
|
||||
useApi,
|
||||
} from '@backstage/core';
|
||||
import { Grid } from '@material-ui/core';
|
||||
import { InfoCard, Page, pageTheme, Content, useApi } from '@backstage/core';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { kubernetesApiRef } from '../../api/types';
|
||||
import { ObjectsByServiceIdResponse } from '@backstage/plugin-kubernetes-backend';
|
||||
|
||||
// TODO this is a temporary component used to construct the Kubernetes plugin boilerplate
|
||||
|
||||
export const KubernetesContent: FC<{ entity: Entity }> = ({ entity }) => {
|
||||
const kubernetesApi = useApi(kubernetesApiRef);
|
||||
const [kubernetesObjects, setKubernetesObjects] = useState<{} | undefined>(
|
||||
undefined,
|
||||
);
|
||||
const [kubernetesObjects, setKubernetesObjects] = useState<
|
||||
ObjectsByServiceIdResponse | undefined
|
||||
>(undefined);
|
||||
const [error, setError] = useState<string | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -50,21 +44,28 @@ export const KubernetesContent: FC<{ entity: Entity }> = ({ entity }) => {
|
||||
return (
|
||||
<Page theme={pageTheme.tool}>
|
||||
<Content>
|
||||
<ContentHeader title="This is where you would see your kubernetes objects" />
|
||||
<Grid container spacing={3} direction="column">
|
||||
<Grid item>
|
||||
<InfoCard title="This is where you would see your kubernetes objects">
|
||||
<Typography variant="body1">
|
||||
{kubernetesObjects === undefined && <div>loading....</div>}
|
||||
{error !== undefined && <div>{error}</div>}
|
||||
{kubernetesObjects !== undefined && (
|
||||
<div>
|
||||
backend response: {JSON.stringify(kubernetesObjects)}
|
||||
</div>
|
||||
)}
|
||||
</Typography>
|
||||
</InfoCard>
|
||||
</Grid>
|
||||
{kubernetesObjects === undefined && <div>loading....</div>}
|
||||
{error !== undefined && <div>{error}</div>}
|
||||
{kubernetesObjects !== undefined && (
|
||||
<div>
|
||||
{kubernetesObjects.items.map((item, i) => (
|
||||
<Grid item key={i}>
|
||||
<InfoCard key={item.cluster.name} title={item.cluster.name}>
|
||||
{item.resources.map((fr, j) => (
|
||||
<div key={j}>
|
||||
<br />
|
||||
{fr.type}:{' '}
|
||||
{(fr.resources as any)
|
||||
.map((v: any) => v.metadata.name)
|
||||
.join(' ')}
|
||||
</div>
|
||||
))}
|
||||
</InfoCard>
|
||||
</Grid>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Grid>
|
||||
</Content>
|
||||
</Page>
|
||||
|
||||
@@ -2441,6 +2441,34 @@
|
||||
resolved "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz#9df03bbd7c696a5c58885c34aa06da41c8543796"
|
||||
integrity sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==
|
||||
|
||||
"@kubernetes/client-node@^0.12.1":
|
||||
version "0.12.2"
|
||||
resolved "https://registry.npmjs.org/@kubernetes/client-node/-/client-node-0.12.2.tgz#8728684bd57d1cbcbe14fe742e79be7021403eea"
|
||||
integrity sha512-J0UwyFl1Iv/IZ6WMP7LaizBEoKPnqwtc8tIO2q/X+EuDT7eGpPPAMHXSEOC/EI9JGIf0FaJEcDHhB/Dio/mKhw==
|
||||
dependencies:
|
||||
"@types/js-yaml" "^3.12.1"
|
||||
"@types/node" "^10.12.0"
|
||||
"@types/request" "^2.47.1"
|
||||
"@types/stream-buffers" "^3.0.3"
|
||||
"@types/tar" "^4.0.3"
|
||||
"@types/underscore" "^1.8.9"
|
||||
"@types/ws" "^6.0.1"
|
||||
byline "^5.0.0"
|
||||
execa "1.0.0"
|
||||
isomorphic-ws "^4.0.1"
|
||||
js-yaml "^3.13.1"
|
||||
jsonpath-plus "^0.19.0"
|
||||
openid-client "^4.1.1"
|
||||
request "^2.88.0"
|
||||
rfc4648 "^1.3.0"
|
||||
shelljs "^0.8.2"
|
||||
stream-buffers "^3.0.2"
|
||||
tar "^6.0.2"
|
||||
tmp-promise "^3.0.2"
|
||||
tslib "^1.9.3"
|
||||
underscore "^1.9.1"
|
||||
ws "^7.3.1"
|
||||
|
||||
"@kyleshockey/object-assign-deep@^0.4.2":
|
||||
version "0.4.2"
|
||||
resolved "https://registry.npmjs.org/@kyleshockey/object-assign-deep/-/object-assign-deep-0.4.2.tgz#84900f0eefc372798f4751b5262830b8208922ec"
|
||||
@@ -4638,6 +4666,11 @@
|
||||
"@types/node" "*"
|
||||
"@types/responselike" "*"
|
||||
|
||||
"@types/caseless@*":
|
||||
version "0.12.2"
|
||||
resolved "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.2.tgz#f65d3d6389e01eeb458bd54dc8f52b95a9463bc8"
|
||||
integrity sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w==
|
||||
|
||||
"@types/classnames@^2.2.9":
|
||||
version "2.2.10"
|
||||
resolved "https://registry.npmjs.org/@types/classnames/-/classnames-2.2.10.tgz#cc658ca319b6355399efc1f5b9e818f1a24bf999"
|
||||
@@ -4995,7 +5028,7 @@
|
||||
resolved "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-2.2.6.tgz#f1a1cb35aff47bc5cfb05cb0c441ca91e914c26f"
|
||||
integrity sha512-+oY0FDTO2GYKEV0YPvSshGq9t7YozVkgvXLty7zogQNuCxBhT9/3INX9Q7H1aRZ4SUDRXAKlJuA4EA5nTt7SNw==
|
||||
|
||||
"@types/js-yaml@^3.12.5":
|
||||
"@types/js-yaml@^3.12.1", "@types/js-yaml@^3.12.5":
|
||||
version "3.12.5"
|
||||
resolved "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-3.12.5.tgz#136d5e6a57a931e1cce6f9d8126aa98a9c92a6bb"
|
||||
integrity sha512-JCcp6J0GV66Y4ZMDAQCXot4xprYB+Zfd3meK9+INSJeVZwJmHAW30BBEEkPzXswMXuiyReUGOP3GxrADc9wPww==
|
||||
@@ -5139,6 +5172,11 @@
|
||||
resolved "https://registry.npmjs.org/@types/node/-/node-10.17.28.tgz#0e36d718a29355ee51cec83b42d921299200f6d9"
|
||||
integrity sha512-dzjES1Egb4c1a89C7lKwQh8pwjYmlOAG9dW1pBgxEk57tMrLnssOfEthz8kdkNaBd7lIqQx7APm5+mZ619IiCQ==
|
||||
|
||||
"@types/node@^10.12.0":
|
||||
version "10.17.35"
|
||||
resolved "https://registry.npmjs.org/@types/node/-/node-10.17.35.tgz#58058f29b870e6ae57b20e4f6e928f02b7129f56"
|
||||
integrity sha512-gXx7jAWpMddu0f7a+L+txMplp3FnHl53OhQIF9puXKq3hDGY/GjH+MF04oWnV/adPSCrbtHumDCFwzq2VhltWA==
|
||||
|
||||
"@types/node@^12.0.0":
|
||||
version "12.12.53"
|
||||
resolved "https://registry.npmjs.org/@types/node/-/node-12.12.53.tgz#be0d375933c3d15ef2380dafb3b0350ea7021129"
|
||||
@@ -5387,6 +5425,16 @@
|
||||
resolved "https://registry.npmjs.org/@types/relateurl/-/relateurl-0.2.28.tgz#6bda7db8653fa62643f5ee69e9f69c11a392e3a6"
|
||||
integrity sha1-a9p9uGU/piZD9e5p6facEaOS46Y=
|
||||
|
||||
"@types/request@^2.47.1":
|
||||
version "2.48.5"
|
||||
resolved "https://registry.npmjs.org/@types/request/-/request-2.48.5.tgz#019b8536b402069f6d11bee1b2c03e7f232937a0"
|
||||
integrity sha512-/LO7xRVnL3DxJ1WkPGDQrp4VTV1reX9RkC85mJ+Qzykj2Bdw+mG15aAfDahc76HtknjzE16SX/Yddn6MxVbmGQ==
|
||||
dependencies:
|
||||
"@types/caseless" "*"
|
||||
"@types/node" "*"
|
||||
"@types/tough-cookie" "*"
|
||||
form-data "^2.5.0"
|
||||
|
||||
"@types/resolve@1.17.1":
|
||||
version "1.17.1"
|
||||
resolved "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz#3afd6ad8967c77e4376c598a82ddd58f46ec45d6"
|
||||
@@ -5472,6 +5520,13 @@
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/stream-buffers@^3.0.3":
|
||||
version "3.0.3"
|
||||
resolved "https://registry.npmjs.org/@types/stream-buffers/-/stream-buffers-3.0.3.tgz#34e565bf64e3e4bdeee23fd4aa58d4636014a02b"
|
||||
integrity sha512-NeFeX7YfFZDYsCfbuaOmFQ0OjSmHreKBpp7MQ4alWQBHeh2USLsj7qyMyn9t82kjqIX516CR/5SRHnARduRtbQ==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/styled-jsx@^2.2.8":
|
||||
version "2.2.8"
|
||||
resolved "https://registry.npmjs.org/@types/styled-jsx/-/styled-jsx-2.2.8.tgz#b50d13d8a3c34036282d65194554cf186bab7234"
|
||||
@@ -5550,6 +5605,11 @@
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/tough-cookie@*":
|
||||
version "4.0.0"
|
||||
resolved "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.0.tgz#fef1904e4668b6e5ecee60c52cc6a078ffa6697d"
|
||||
integrity sha512-I99sngh224D0M7XgW1s120zxCt3VYQ3IQsuw3P3jbq5GG4yc79+ZjyKznyOGIQrflfylLgcfekeZW/vk0yng6A==
|
||||
|
||||
"@types/uglify-js@*":
|
||||
version "3.0.4"
|
||||
resolved "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.0.4.tgz#96beae23df6f561862a830b4288a49e86baac082"
|
||||
@@ -5557,6 +5617,11 @@
|
||||
dependencies:
|
||||
source-map "^0.6.1"
|
||||
|
||||
"@types/underscore@^1.8.9":
|
||||
version "1.10.23"
|
||||
resolved "https://registry.npmjs.org/@types/underscore/-/underscore-1.10.23.tgz#cc672e8864000d288e1e39c609fd9cab84391ff3"
|
||||
integrity sha512-vX1NPekXhrLquFWskH2thcvFAha187F/lM6xYOoEMZWwJ/6alSk0/ttmGP/YRqcqtCv0TMbZjYAdZyHAEcuU4g==
|
||||
|
||||
"@types/uuid@^8.0.0":
|
||||
version "8.0.0"
|
||||
resolved "https://registry.npmjs.org/@types/uuid/-/uuid-8.0.0.tgz#165aae4819ad2174a17476dbe66feebd549556c0"
|
||||
@@ -5618,6 +5683,13 @@
|
||||
resolved "https://registry.npmjs.org/@types/whatwg-streams/-/whatwg-streams-0.0.7.tgz#28bfe73dc850562296367249c4b32a50db81e9d3"
|
||||
integrity sha512-6sDiSEP6DWcY2ZolsJ2s39ZmsoGQ7KVwBDI3sESQsEm9P2dHTcqnDIHRZFRNtLCzWp7hCFGqYbw5GyfpQnJ01A==
|
||||
|
||||
"@types/ws@^6.0.1":
|
||||
version "6.0.4"
|
||||
resolved "https://registry.npmjs.org/@types/ws/-/ws-6.0.4.tgz#7797707c8acce8f76d8c34b370d4645b70421ff1"
|
||||
integrity sha512-PpPrX7SZW9re6+Ha8ojZG4Se8AZXgf0GK6zmfqEuCsY49LFDNXO3SByp44X3dFEqtB73lkCDAdUazhAjVPiNwg==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/ws@^7.0.0":
|
||||
version "7.2.6"
|
||||
resolved "https://registry.npmjs.org/@types/ws/-/ws-7.2.6.tgz#516cbfb818310f87b43940460e065eb912a4178d"
|
||||
@@ -7156,7 +7228,7 @@ base64-js@^1.0.2, base64-js@^1.2.0:
|
||||
resolved "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1"
|
||||
integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==
|
||||
|
||||
base64url@3.x.x:
|
||||
base64url@3.x.x, base64url@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz#6399d572e2bc3f90a9a8b22d5dbb0a32d33f788d"
|
||||
integrity sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==
|
||||
@@ -10630,6 +10702,19 @@ exec-sh@^0.3.2:
|
||||
resolved "https://registry.npmjs.org/exec-sh/-/exec-sh-0.3.4.tgz#3a018ceb526cc6f6df2bb504b2bfe8e3a4934ec5"
|
||||
integrity sha512-sEFIkc61v75sWeOe72qyrqg2Qg0OuLESziUDk/O/z2qgS15y2gWVFrI6f2Qn/qw/0/NCfCEsmNA4zOjkwEZT1A==
|
||||
|
||||
execa@1.0.0, execa@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8"
|
||||
integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==
|
||||
dependencies:
|
||||
cross-spawn "^6.0.0"
|
||||
get-stream "^4.0.0"
|
||||
is-stream "^1.1.0"
|
||||
npm-run-path "^2.0.0"
|
||||
p-finally "^1.0.0"
|
||||
signal-exit "^3.0.0"
|
||||
strip-eof "^1.0.0"
|
||||
|
||||
execa@3.4.0:
|
||||
version "3.4.0"
|
||||
resolved "https://registry.npmjs.org/execa/-/execa-3.4.0.tgz#c08ed4550ef65d858fac269ffc8572446f37eb89"
|
||||
@@ -10659,19 +10744,6 @@ execa@^0.7.0:
|
||||
signal-exit "^3.0.0"
|
||||
strip-eof "^1.0.0"
|
||||
|
||||
execa@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8"
|
||||
integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==
|
||||
dependencies:
|
||||
cross-spawn "^6.0.0"
|
||||
get-stream "^4.0.0"
|
||||
is-stream "^1.1.0"
|
||||
npm-run-path "^2.0.0"
|
||||
p-finally "^1.0.0"
|
||||
signal-exit "^3.0.0"
|
||||
strip-eof "^1.0.0"
|
||||
|
||||
execa@^4.0.0, execa@^4.0.1:
|
||||
version "4.0.2"
|
||||
resolved "https://registry.npmjs.org/execa/-/execa-4.0.2.tgz#ad87fb7b2d9d564f70d2b62d511bee41d5cbb240"
|
||||
@@ -11337,7 +11409,7 @@ fork-ts-checker-webpack-plugin@^4.0.5, fork-ts-checker-webpack-plugin@^4.1.4:
|
||||
tapable "^1.0.0"
|
||||
worker-rpc "^0.1.0"
|
||||
|
||||
form-data@^2.3.1, form-data@^2.3.2:
|
||||
form-data@^2.3.1, form-data@^2.3.2, form-data@^2.5.0:
|
||||
version "2.5.1"
|
||||
resolved "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz#f2cbec57b5e59e23716e128fe44d4e5dd23895f4"
|
||||
integrity sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==
|
||||
@@ -11982,6 +12054,23 @@ got@^11.5.2:
|
||||
p-cancelable "^2.0.0"
|
||||
responselike "^2.0.0"
|
||||
|
||||
got@^11.6.2:
|
||||
version "11.7.0"
|
||||
resolved "https://registry.npmjs.org/got/-/got-11.7.0.tgz#a386360305571a74548872e674932b4ef70d3b24"
|
||||
integrity sha512-7en2XwH2MEqOsrK0xaKhbWibBoZqy+f1RSUoIeF1BLcnf+pyQdDsljWMfmOh+QKJwuvDIiKx38GtPh5wFdGGjg==
|
||||
dependencies:
|
||||
"@sindresorhus/is" "^3.1.1"
|
||||
"@szmarczak/http-timer" "^4.0.5"
|
||||
"@types/cacheable-request" "^6.0.1"
|
||||
"@types/responselike" "^1.0.0"
|
||||
cacheable-lookup "^5.0.3"
|
||||
cacheable-request "^7.0.1"
|
||||
decompress-response "^6.0.0"
|
||||
http2-wrapper "^1.0.0-beta.5.2"
|
||||
lowercase-keys "^2.0.0"
|
||||
p-cancelable "^2.0.0"
|
||||
responselike "^2.0.0"
|
||||
|
||||
got@^9.6.0:
|
||||
version "9.6.0"
|
||||
resolved "https://registry.npmjs.org/got/-/got-9.6.0.tgz#edf45e7d67f99545705de1f7bbeeeb121765ed85"
|
||||
@@ -13604,6 +13693,11 @@ isomorphic-form-data@~2.0.0:
|
||||
dependencies:
|
||||
form-data "^2.3.2"
|
||||
|
||||
isomorphic-ws@^4.0.1:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz#55fd4cd6c5e6491e76dc125938dd863f5cd4f2dc"
|
||||
integrity sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==
|
||||
|
||||
isstream@~0.1.2:
|
||||
version "0.1.2"
|
||||
resolved "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
|
||||
@@ -14110,6 +14204,13 @@ jose@^1.27.1:
|
||||
dependencies:
|
||||
"@panva/asn1.js" "^1.0.0"
|
||||
|
||||
jose@^2.0.2:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.npmjs.org/jose/-/jose-2.0.2.tgz#fb22385b80c658cc7a0cae05b7086c04c6be49f4"
|
||||
integrity sha512-yD93lsiMA1go/qxSY/vXWBodmIZJIxeB7QhFi8z1yQ3KUwKENqI9UA8VCHlQ5h3x1zWuWZjoY87ByQzkQbIrQg==
|
||||
dependencies:
|
||||
"@panva/asn1.js" "^1.0.0"
|
||||
|
||||
js-cookie@^2.2.1:
|
||||
version "2.2.1"
|
||||
resolved "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.1.tgz#69e106dc5d5806894562902aa5baec3744e9b2b8"
|
||||
@@ -14402,6 +14503,11 @@ jsonparse@^1.2.0:
|
||||
resolved "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280"
|
||||
integrity sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=
|
||||
|
||||
jsonpath-plus@^0.19.0:
|
||||
version "0.19.0"
|
||||
resolved "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-0.19.0.tgz#b901e57607055933dc9a8bef0cc25160ee9dd64c"
|
||||
integrity sha512-GSVwsrzW9LsA5lzsqe4CkuZ9wp+kxBb2GwNniaWzI2YFn5Ig42rSW8ZxVpWXaAfakXNrx5pgY5AbQq7kzX29kg==
|
||||
|
||||
jsonpointer@^4.0.1:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz#4fd92cb34e0e9db3c89c8622ecf51f9b978c6cb9"
|
||||
@@ -15297,7 +15403,7 @@ make-dir@^3.0.0, make-dir@^3.0.2:
|
||||
dependencies:
|
||||
semver "^6.0.0"
|
||||
|
||||
make-error@1.x, make-error@^1.1.1:
|
||||
make-error@1.x, make-error@^1.1.1, make-error@^1.3.6:
|
||||
version "1.3.6"
|
||||
resolved "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2"
|
||||
integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==
|
||||
@@ -16500,6 +16606,11 @@ object-copy@^0.1.0:
|
||||
define-property "^0.2.5"
|
||||
kind-of "^3.0.3"
|
||||
|
||||
object-hash@^2.0.1:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.npmjs.org/object-hash/-/object-hash-2.0.3.tgz#d12db044e03cd2ca3d77c0570d87225b02e1e6ea"
|
||||
integrity sha512-JPKn0GMu+Fa3zt3Bmr66JhokJU5BaNBIh4ZeTlaCBzrBsOeXzwcKKAK1tbLiPKgvwmPXsDvvLHoWh5Bm7ofIYg==
|
||||
|
||||
object-inspect@^1.7.0:
|
||||
version "1.7.0"
|
||||
resolved "https://registry.npmjs.org/object-inspect/-/object-inspect-1.7.0.tgz#f4f6bd181ad77f006b5ece60bd0b6f398ff74a67"
|
||||
@@ -16615,6 +16726,11 @@ octokit-pagination-methods@^1.1.0:
|
||||
resolved "https://registry.npmjs.org/octokit-pagination-methods/-/octokit-pagination-methods-1.1.0.tgz#cf472edc9d551055f9ef73f6e42b4dbb4c80bea4"
|
||||
integrity sha512-fZ4qZdQ2nxJvtcasX7Ghl+WlWS/d9IgnBIwFZXVNNZUmzpno91SX5bc5vuxiuKoCtK78XxGGNuSCrDC7xYB3OQ==
|
||||
|
||||
oidc-token-hash@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.0.tgz#acdfb1f4310f58e64d5d74a4e8671a426986e888"
|
||||
integrity sha512-8Yr4CZSv+Tn8ZkN3iN2i2w2G92mUKClp4z7EGUfdsERiYSbj7P4i/NHm72ft+aUdsiFx9UdIPSTwbyzQ6C4URg==
|
||||
|
||||
omggif@1.0.7:
|
||||
version "1.0.7"
|
||||
resolved "https://registry.npmjs.org/omggif/-/omggif-1.0.7.tgz#59d2eecb0263de84635b3feb887c0c9973f1e49d"
|
||||
@@ -16683,6 +16799,20 @@ opencollective-postinstall@^2.0.2:
|
||||
resolved "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.2.tgz#5657f1bede69b6e33a45939b061eb53d3c6c3a89"
|
||||
integrity sha512-pVOEP16TrAO2/fjej1IdOyupJY8KDUM1CvsaScRbw6oddvpQoOfGk4ywha0HKKVAD6RkW4x6Q+tNBwhf3Bgpuw==
|
||||
|
||||
openid-client@^4.1.1:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.npmjs.org/openid-client/-/openid-client-4.1.1.tgz#3e8a25584c4292e9b9b03e60358f5549fb85197a"
|
||||
integrity sha512-/qch3I3v8UtO0A7wVgyXJJjGX/knR8bv06DQpLuKQqLG5u4AHcgusGuVKPKAcneLZvHKbKovF2+3e2ngXyuudA==
|
||||
dependencies:
|
||||
base64url "^3.0.1"
|
||||
got "^11.6.2"
|
||||
jose "^2.0.2"
|
||||
lru-cache "^6.0.0"
|
||||
make-error "^1.3.6"
|
||||
object-hash "^2.0.1"
|
||||
oidc-token-hash "^5.0.0"
|
||||
p-any "^3.0.0"
|
||||
|
||||
opn@^5.5.0:
|
||||
version "5.5.0"
|
||||
resolved "https://registry.npmjs.org/opn/-/opn-5.5.0.tgz#fc7164fab56d235904c51c3b27da6758ca3b9bfc"
|
||||
@@ -16788,6 +16918,14 @@ overlayscrollbars@^1.10.2:
|
||||
resolved "https://registry.npmjs.org/overlayscrollbars/-/overlayscrollbars-1.13.0.tgz#1edb436328133b94877b558f77966d5497ca36a7"
|
||||
integrity sha512-p8oHrMeRAKxXDMPI/EBNITj/zTVHKNnAnM59Im+xnoZUlV07FyTg46wom2286jJlXGGfcPFG/ba5NUiCwWNd4w==
|
||||
|
||||
p-any@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.npmjs.org/p-any/-/p-any-3.0.0.tgz#79847aeed70b5d3a10ea625296c0c3d2e90a87b9"
|
||||
integrity sha512-5rqbqfsRWNb0sukt0awwgJMlaep+8jV45S15SKKB34z4UuzjcofIfnriCBhWjZP2jbVtjt9yRl7buB6RlKsu9w==
|
||||
dependencies:
|
||||
p-cancelable "^2.0.0"
|
||||
p-some "^5.0.0"
|
||||
|
||||
p-cancelable@^1.0.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz#d078d15a3af409220c886f1d9a0ca2e441ab26cc"
|
||||
@@ -16934,6 +17072,14 @@ p-retry@^3.0.1:
|
||||
dependencies:
|
||||
retry "^0.12.0"
|
||||
|
||||
p-some@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.npmjs.org/p-some/-/p-some-5.0.0.tgz#8b730c74b4fe5169d7264a240ad010b6ebc686a4"
|
||||
integrity sha512-Js5XZxo6vHjB9NOYAzWDYAIyyiPvva0DWESAIWIK7uhSpGsyg5FwUPxipU/SOQx5x9EqhOh545d1jo6cVkitig==
|
||||
dependencies:
|
||||
aggregate-error "^3.0.0"
|
||||
p-cancelable "^2.0.0"
|
||||
|
||||
p-timeout@^3.1.0:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz#c7e17abc971d2a7962ef83626b35d635acf23dfe"
|
||||
@@ -19764,6 +19910,11 @@ reusify@^1.0.4:
|
||||
resolved "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
|
||||
integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
|
||||
|
||||
rfc4648@^1.3.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.npmjs.org/rfc4648/-/rfc4648-1.4.0.tgz#c75b2856ad2e2d588b6ddb985d556f1f7f2a2abd"
|
||||
integrity sha512-3qIzGhHlMHA6PoT6+cdPKZ+ZqtxkIvg8DZGKA5z6PQ33/uuhoJ+Ws/D/J9rXW6gXodgH8QYlz2UCl+sdUDmNIg==
|
||||
|
||||
rgb-regex@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.npmjs.org/rgb-regex/-/rgb-regex-1.0.1.tgz#c0e0d6882df0e23be254a475e8edd41915feaeb1"
|
||||
@@ -20303,7 +20454,7 @@ shell-quote@1.7.2:
|
||||
resolved "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.2.tgz#67a7d02c76c9da24f99d20808fcaded0e0e04be2"
|
||||
integrity sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg==
|
||||
|
||||
shelljs@^0.8.3:
|
||||
shelljs@^0.8.2, shelljs@^0.8.3:
|
||||
version "0.8.4"
|
||||
resolved "https://registry.npmjs.org/shelljs/-/shelljs-0.8.4.tgz#de7684feeb767f8716b326078a8a00875890e3c2"
|
||||
integrity sha512-7gk3UZ9kOfPLIAbslLzyWeGiEqx9e3rxwZM0KE6EL8GlGwjym9Mrlx5/p33bWTu9YG6vcS4MBxYZDHYr5lr8BQ==
|
||||
@@ -20867,6 +21018,11 @@ stream-browserify@^2.0.1:
|
||||
inherits "~2.0.1"
|
||||
readable-stream "^2.0.2"
|
||||
|
||||
stream-buffers@^3.0.2:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.npmjs.org/stream-buffers/-/stream-buffers-3.0.2.tgz#5249005a8d5c2d00b3a32e6e0a6ea209dc4f3521"
|
||||
integrity sha512-DQi1h8VEBA/lURbSwFtEHnSTb9s2/pwLEaFuNhXwy1Dx3Sa0lOuYT2yNUr4/j2fs8oCAMANtrZ5OrPZtyVs3MQ==
|
||||
|
||||
stream-combiner@~0.0.4:
|
||||
version "0.0.4"
|
||||
resolved "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.0.4.tgz#4d5e433c185261dde623ca3f44c586bcf5c4ad14"
|
||||
@@ -21731,6 +21887,13 @@ tinycolor2@^1.4.1:
|
||||
resolved "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.1.tgz#f4fad333447bc0b07d4dc8e9209d8f39a8ac77e8"
|
||||
integrity sha1-9PrTM0R7wLB9TcjpIJ2POaisd+g=
|
||||
|
||||
tmp-promise@^3.0.2:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.2.tgz#6e933782abff8b00c3119d63589ca1fb9caaa62a"
|
||||
integrity sha512-OyCLAKU1HzBjL6Ev3gxUeraJNlbNingmi8IrHHEsYH8LTmEuhvYfqvhn2F/je+mjf4N58UmZ96OMEy1JanSCpA==
|
||||
dependencies:
|
||||
tmp "^0.2.0"
|
||||
|
||||
tmp@^0.0.33:
|
||||
version "0.0.33"
|
||||
resolved "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"
|
||||
@@ -21738,6 +21901,13 @@ tmp@^0.0.33:
|
||||
dependencies:
|
||||
os-tmpdir "~1.0.2"
|
||||
|
||||
tmp@^0.2.0:
|
||||
version "0.2.1"
|
||||
resolved "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14"
|
||||
integrity sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==
|
||||
dependencies:
|
||||
rimraf "^3.0.0"
|
||||
|
||||
tmp@~0.1.0:
|
||||
version "0.1.0"
|
||||
resolved "https://registry.npmjs.org/tmp/-/tmp-0.1.0.tgz#ee434a4e22543082e294ba6201dcc6eafefa2877"
|
||||
@@ -22190,6 +22360,11 @@ undefsafe@^2.0.2:
|
||||
dependencies:
|
||||
debug "^2.2.0"
|
||||
|
||||
underscore@^1.9.1:
|
||||
version "1.11.0"
|
||||
resolved "https://registry.npmjs.org/underscore/-/underscore-1.11.0.tgz#dd7c23a195db34267186044649870ff1bab5929e"
|
||||
integrity sha512-xY96SsN3NA461qIRKZ/+qox37YXPtSBswMGfiNptr+wrt6ds4HaMw23TP612fEyGekRE6LNRiLYr/aqbHXNedw==
|
||||
|
||||
unfetch@^4.1.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.npmjs.org/unfetch/-/unfetch-4.1.0.tgz#6ec2dd0de887e58a4dee83a050ded80ffc4137db"
|
||||
@@ -23243,6 +23418,11 @@ ws@^7.2.3:
|
||||
resolved "https://registry.npmjs.org/ws/-/ws-7.3.0.tgz#4b2f7f219b3d3737bc1a2fbf145d825b94d38ffd"
|
||||
integrity sha512-iFtXzngZVXPGgpTlP1rBqsUK82p9tKqsWRPg5L56egiljujJT3vGAYnHANvFxBieXrTFavhzhxW52jnaWV+w2w==
|
||||
|
||||
ws@^7.3.1:
|
||||
version "7.3.1"
|
||||
resolved "https://registry.npmjs.org/ws/-/ws-7.3.1.tgz#d0547bf67f7ce4f12a72dfe31262c68d7dc551c8"
|
||||
integrity sha512-D3RuNkynyHmEJIpD2qrgVkc9DQ23OrN/moAwZX4L8DfvszsJxpjQuUq3LMx6HoYji9fbIOBY18XWBsAux1ZZUA==
|
||||
|
||||
x-is-string@^0.1.0:
|
||||
version "0.1.0"
|
||||
resolved "https://registry.npmjs.org/x-is-string/-/x-is-string-0.1.0.tgz#474b50865af3a49a9c4657f05acd145458f77d82"
|
||||
|
||||
Reference in New Issue
Block a user