k8s add hpa and ingresses UI (#2640)

* k8s add hpa and ingresses UI

* add todo
This commit is contained in:
Matthew Clarke
2020-09-28 19:32:35 +01:00
committed by GitHub
parent 964dbd0dad
commit d66c6a7dcb
19 changed files with 632 additions and 36 deletions
@@ -7,7 +7,9 @@ An app to roll dice (it doesn't actually do that).
## Prerequisites
- kubectl installed
- Minikube installed
- Minikube installed, with the following addons
- metrics-server
- ingress
- jq installed
- Backstage locally built and ready to run
@@ -53,6 +53,41 @@ spec:
ports:
- containerPort: 82
---
apiVersion: autoscaling/v1
kind: HorizontalPodAutoscaler
metadata:
name: dice-roller
labels:
'backstage.io/kubernetes-id': dice-roller
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: dice-roller
minReplicas: 10
maxReplicas: 15
targetCPUUtilizationPercentage: 50
---
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
name: dice-roller
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /$1
labels:
'backstage.io/kubernetes-id': dice-roller
spec:
rules:
- host: nginx
http:
paths:
- path: /
backend:
serviceName: dice-roller
servicePort: 80
---
apiVersion: v1
kind: ConfigMap
@@ -15,7 +15,13 @@
*/
import { ClusterDetails } from '..';
import { AppsV1Api, CoreV1Api, KubeConfig } from '@kubernetes/client-node';
import {
AppsV1Api,
AutoscalingV1Api,
CoreV1Api,
KubeConfig,
NetworkingV1beta1Api,
} from '@kubernetes/client-node';
export class KubernetesClientProvider {
private readonly coreClientMap: {
@@ -26,9 +32,19 @@ export class KubernetesClientProvider {
[key: string]: AppsV1Api;
};
private readonly autoscalingClientMap: {
[key: string]: AutoscalingV1Api;
};
private readonly networkingBeta1ClientMap: {
[key: string]: NetworkingV1beta1Api;
};
constructor() {
this.coreClientMap = {};
this.appsClientMap = {};
this.autoscalingClientMap = {};
this.networkingBeta1ClientMap = {};
}
// visible for testing
@@ -93,4 +109,36 @@ export class KubernetesClientProvider {
return client;
}
getAutoscalingClientByClusterDetails(clusterDetails: ClusterDetails) {
const clientMapKey = clusterDetails.name;
if (this.autoscalingClientMap.hasOwnProperty(clientMapKey)) {
return this.autoscalingClientMap[clientMapKey];
}
const kc = this.getKubeConfig(clusterDetails);
const client = kc.makeApiClient(AutoscalingV1Api);
this.autoscalingClientMap[clientMapKey] = client;
return client;
}
getNetworkingBeta1Client(clusterDetails: ClusterDetails) {
const clientMapKey = clusterDetails.name;
if (this.networkingBeta1ClientMap.hasOwnProperty(clientMapKey)) {
return this.networkingBeta1ClientMap[clientMapKey];
}
const kc = this.getKubeConfig(clusterDetails);
const client = kc.makeApiClient(NetworkingV1beta1Api);
this.networkingBeta1ClientMap[clientMapKey] = client;
return client;
}
}
@@ -31,6 +31,8 @@ describe('KubernetesClientProvider', () => {
const kubernetesClientProvider: any = {
getCoreClientByClusterDetails: jest.fn(() => clientMock),
getAppsClientByClusterDetails: jest.fn(() => clientMock),
getAutoscalingClientByClusterDetails: jest.fn(() => clientMock),
getNetworkingBeta1Client: jest.fn(() => clientMock),
};
const sut = new KubernetesClientBasedFetcher({
@@ -16,12 +16,15 @@
import {
AppsV1Api,
AutoscalingV1Api,
CoreV1Api,
ExtensionsV1beta1Ingress,
NetworkingV1beta1Api,
V1ConfigMap,
V1Deployment,
V1HorizontalPodAutoscaler,
V1Pod,
V1ReplicaSet,
V1Secret,
} from '@kubernetes/client-node';
import { KubernetesClientProvider } from './KubernetesClientProvider';
import { V1Service } from '@kubernetes/client-node/dist/gen/model/v1Service';
@@ -36,6 +39,8 @@ import {
export interface Clients {
core: CoreV1Api;
apps: AppsV1Api;
autoscaling: AutoscalingV1Api;
networkingBeta1: NetworkingV1beta1Api;
}
export interface KubernetesClientBasedFetcherOptions {
@@ -67,6 +72,7 @@ export class KubernetesClientBasedFetcher implements KubernetesFetcher {
);
}
// TODO could probably do with a tidy up
private fetchByObjectType(
serviceId: string,
clusterDetails: ClusterDetails,
@@ -93,13 +99,18 @@ export class KubernetesClientBasedFetcher implements KubernetesFetcher {
serviceId,
clusterDetails,
).then(r => ({ type: type, resources: r }));
case 'secrets':
return this.fetchSecretsByServiceId(
case 'services':
return this.fetchServicesByServiceId(
serviceId,
clusterDetails,
).then(r => ({ type: type, resources: r }));
case 'services':
return this.fetchServicesByServiceId(
case 'horizontalpodautoscalers':
return this.fetchHorizontalPodAutoscalersByServiceId(
serviceId,
clusterDetails,
).then(r => ({ type: type, resources: r }));
case 'ingresses':
return this.fetchIngressesByServiceId(
serviceId,
clusterDetails,
).then(r => ({ type: type, resources: r }));
@@ -119,9 +130,15 @@ export class KubernetesClientBasedFetcher implements KubernetesFetcher {
const apps = this.kubernetesClientProvider.getAppsClientByClusterDetails(
clusterDetails,
);
const autoscaling = this.kubernetesClientProvider.getAutoscalingClientByClusterDetails(
clusterDetails,
);
const networkingBeta1 = this.kubernetesClientProvider.getNetworkingBeta1Client(
clusterDetails,
);
this.logger.debug(`calling cluster=${clusterDetails.name}`);
return fn({ core, apps }).then(result => {
return fn({ core, apps, autoscaling, networkingBeta1 }).then(result => {
return result.body.items;
});
}
@@ -168,20 +185,6 @@ export class KubernetesClientBasedFetcher implements KubernetesFetcher {
);
}
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,
@@ -209,4 +212,36 @@ export class KubernetesClientBasedFetcher implements KubernetesFetcher {
),
);
}
private fetchHorizontalPodAutoscalersByServiceId(
serviceId: string,
clusterDetails: ClusterDetails,
): Promise<Array<V1HorizontalPodAutoscaler>> {
return this.singleClusterFetch<V1HorizontalPodAutoscaler>(
clusterDetails,
({ autoscaling }) =>
autoscaling.listHorizontalPodAutoscalerForAllNamespaces(
false,
'',
'',
`backstage.io/kubernetes-id=${serviceId}`,
),
);
}
private fetchIngressesByServiceId(
serviceId: string,
clusterDetails: ClusterDetails,
): Promise<Array<ExtensionsV1beta1Ingress>> {
return this.singleClusterFetch<ExtensionsV1beta1Ingress>(
clusterDetails,
({ networkingBeta1 }) =>
networkingBeta1.listIngressForAllNamespaces(
false,
'',
'',
`backstage.io/kubernetes-id=${serviceId}`,
),
);
}
}
@@ -36,6 +36,8 @@ const DEFAULT_OBJECTS = new Set<KubernetesObjectTypes>([
'configmaps',
'deployments',
'replicasets',
'horizontalpodautoscalers',
'ingresses',
]);
export const handleGetKubernetesObjectsByServiceId: GetKubernetesObjectsByServiceIdHandler = async (
+18 -10
View File
@@ -15,11 +15,12 @@
*/
import {
ExtensionsV1beta1Ingress,
V1ConfigMap,
V1Deployment,
V1HorizontalPodAutoscaler,
V1Pod,
V1ReplicaSet,
V1Secret,
V1Service,
} from '@kubernetes/client-node';
@@ -43,9 +44,10 @@ export type FetchResponse =
| PodFetchResponse
| ServiceFetchResponse
| ConfigMapFetchResponse
| SecretFetchResponse
| DeploymentFetchResponse
| ReplicaSetsFetchResponse;
| ReplicaSetsFetchResponse
| HorizontalPodAutoscalersFetchResponse
| IngressesFetchResponse;
// TODO fairly sure there's a easier way to do this
@@ -53,9 +55,10 @@ export type KubernetesObjectTypes =
| 'pods'
| 'services'
| 'configmaps'
| 'secrets'
| 'deployments'
| 'replicasets';
| 'replicasets'
| 'horizontalpodautoscalers'
| 'ingresses';
export interface PodFetchResponse {
type: 'pods';
@@ -72,11 +75,6 @@ export interface ConfigMapFetchResponse {
resources: Array<V1ConfigMap>;
}
export interface SecretFetchResponse {
type: 'secrets';
resources: Array<V1Secret>;
}
export interface DeploymentFetchResponse {
type: 'deployments';
resources: Array<V1Deployment>;
@@ -87,6 +85,16 @@ export interface ReplicaSetsFetchResponse {
resources: Array<V1ReplicaSet>;
}
export interface HorizontalPodAutoscalersFetchResponse {
type: 'horizontalpodautoscalers';
resources: Array<V1HorizontalPodAutoscaler>;
}
export interface IngressesFetchResponse {
type: 'ingresses';
resources: Array<ExtensionsV1beta1Ingress>;
}
// Fetches information from a kubernetes cluster using the cluster details object
// to target a specific cluster
export interface KubernetesFetcher {
@@ -30,7 +30,7 @@ describe('ConfigMaps', () => {
// title
expect(getByText('dice-roller')).toBeInTheDocument();
expect(getByText('ConfigMap')).toBeInTheDocument();
expect(getByText('Config Map')).toBeInTheDocument();
// values
expect(getByText('Immutable')).toBeInTheDocument();
@@ -32,7 +32,7 @@ export const ConfigMaps = ({ configMaps }: ConfigMapsProps) => {
<Grid item key={i}>
<InfoCard
title={cm.metadata?.name ?? 'un-named service'}
subheader="ConfigMap"
subheader="Config Map"
>
<div>
<StructuredMetadataTable
@@ -0,0 +1,52 @@
/*
* 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 React from 'react';
import { render } from '@testing-library/react';
import * as hpaFixture from './__fixtures__/horizontalpodautoscalers.json';
import { wrapInTestApp } from '@backstage/test-utils';
import { HorizontalPodAutoscalers } from './HorizontalPodAutoscalers';
describe('HorizontalPodAutoscalers', () => {
it('should render horizontalpodautoscaler', async () => {
const { getByText, getAllByText } = render(
wrapInTestApp(
<HorizontalPodAutoscalers hpas={(hpaFixture as any).default} />,
),
);
// title
expect(getByText('dice-roller')).toBeInTheDocument();
expect(getByText('Horizontal Pod Autoscaler')).toBeInTheDocument();
expect(getByText('Scaling Target')).toBeInTheDocument();
expect(getByText('Api Version: apps/v1')).toBeInTheDocument();
expect(getByText('Kind: Deployment')).toBeInTheDocument();
expect(getByText('Name: dice-roller')).toBeInTheDocument();
expect(getByText('Min Replicas')).toBeInTheDocument();
expect(getByText('Max Replicas')).toBeInTheDocument();
expect(getByText('15')).toBeInTheDocument();
expect(getByText('Current Replicas')).toBeInTheDocument();
expect(getByText('Desired Replicas')).toBeInTheDocument();
expect(getByText('0')).toBeInTheDocument();
expect(getByText('Target CPU Utilization Percentage')).toBeInTheDocument();
expect(getByText('50')).toBeInTheDocument();
expect(getByText('Current CPU Utilization Percentage')).toBeInTheDocument();
expect(getByText('Last Scale Time')).toBeInTheDocument();
expect(getAllByText('unknown')).toHaveLength(2);
expect(getAllByText('10')).toHaveLength(2);
});
});
@@ -0,0 +1,64 @@
/*
* 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 React from 'react';
import { Grid } from '@material-ui/core';
import { V1HorizontalPodAutoscaler } from '@kubernetes/client-node';
import { InfoCard, StructuredMetadataTable } from '@backstage/core';
import { orUnknown } from '../../utils';
type HorizontalPodAutoscalersProps = {
hpas: V1HorizontalPodAutoscaler[];
children?: React.ReactNode;
};
export const HorizontalPodAutoscalers = ({
hpas,
}: HorizontalPodAutoscalersProps) => {
return (
<Grid container>
{hpas.map((hpa, i) => {
return (
<Grid item key={i}>
<InfoCard
title={hpa.metadata?.name ?? 'un-named service'}
subheader="Horizontal Pod Autoscaler"
>
<div>
<StructuredMetadataTable
metadata={{
scalingTarget: orUnknown(hpa.spec?.scaleTargetRef),
minReplicas: orUnknown(hpa.spec?.minReplicas),
maxReplicas: orUnknown(hpa.spec?.maxReplicas),
currentReplicas: orUnknown(hpa.status?.currentReplicas),
desiredReplicas: orUnknown(hpa.status?.desiredReplicas),
targetCPUUtilizationPercentage: orUnknown(
hpa.spec?.targetCPUUtilizationPercentage,
),
currentCPUUtilizationPercentage: orUnknown(
hpa.status?.currentCPUUtilizationPercentage,
),
lastScaleTime: orUnknown(hpa.status?.lastScaleTime),
}}
/>
</div>
</InfoCard>
</Grid>
);
})}
</Grid>
);
};
@@ -0,0 +1,81 @@
[
{
"metadata": {
"annotations": {
"autoscaling.alpha.kubernetes.io/conditions": "[{\"type\":\"AbleToScale\",\"status\":\"True\",\"lastTransitionTime\":\"2020-09-28T13:28:15Z\",\"reason\":\"SucceededGetScale\",\"message\":\"the HPA controller was able to get the target's current scale\"},{\"type\":\"ScalingActive\",\"status\":\"False\",\"lastTransitionTime\":\"2020-09-28T13:28:15Z\",\"reason\":\"FailedGetResourceMetric\",\"message\":\"the HPA was unable to compute the replica count: unable to get metrics for resource cpu: unable to fetch metrics from resource metrics API: the server could not find the requested resource (get pods.metrics.k8s.io)\"}]",
"kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"autoscaling/v1\",\"kind\":\"HorizontalPodAutoscaler\",\"metadata\":{\"annotations\":{},\"labels\":{\"backstage.io/kubernetes-id\":\"dice-roller\"},\"name\":\"dice-roller\",\"namespace\":\"default\"},\"spec\":{\"maxReplicas\":15,\"minReplicas\":10,\"scaleTargetRef\":{\"apiVersion\":\"apps/v1\",\"kind\":\"Deployment\",\"name\":\"dice-roller\"},\"targetCPUUtilizationPercentage\":50}}\n"
},
"creationTimestamp": "2020-09-28T13:28:00.000Z",
"labels": {
"backstage.io/kubernetes-id": "dice-roller"
},
"managedFields": [
{
"apiVersion": "autoscaling/v1",
"fieldsType": "FieldsV1",
"fieldsV1": {
"f:metadata": {
"f:annotations": {
"f:autoscaling.alpha.kubernetes.io/conditions": {}
}
},
"f:status": {
"f:currentReplicas": {}
}
},
"manager": "kube-controller-manager",
"operation": "Update",
"time": "2020-09-28T13:28:15.000Z"
},
{
"apiVersion": "autoscaling/v1",
"fieldsType": "FieldsV1",
"fieldsV1": {
"f:metadata": {
"f:annotations": {
".": {},
"f:kubectl.kubernetes.io/last-applied-configuration": {}
},
"f:labels": {
".": {},
"f:backstage.io/kubernetes-id": {}
}
},
"f:spec": {
"f:maxReplicas": {},
"f:minReplicas": {},
"f:scaleTargetRef": {
"f:apiVersion": {},
"f:kind": {},
"f:name": {}
},
"f:targetCPUUtilizationPercentage": {}
}
},
"manager": "kubectl",
"operation": "Update",
"time": "2020-09-28T13:28:21.000Z"
}
],
"name": "dice-roller",
"namespace": "default",
"resourceVersion": "698957",
"selfLink": "/apis/autoscaling/v1/namespaces/default/horizontalpodautoscalers/dice-roller",
"uid": "a70c8a90-5605-4d7d-adea-05cfb8d9d446"
},
"spec": {
"maxReplicas": 15,
"minReplicas": 10,
"scaleTargetRef": {
"apiVersion": "apps/v1",
"kind": "Deployment",
"name": "dice-roller"
},
"targetCPUUtilizationPercentage": 50
},
"status": {
"currentReplicas": 10,
"desiredReplicas": 0
}
}
]
@@ -0,0 +1,16 @@
/*
* 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 { HorizontalPodAutoscalers } from './HorizontalPodAutoscalers';
@@ -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 React from 'react';
import { render } from '@testing-library/react';
import * as ingressesFixture from './__fixtures__/ingress.json';
import { wrapInTestApp } from '@backstage/test-utils';
import { Ingresses } from './Ingresses';
describe('Ingresses', () => {
it('should render ingress', async () => {
const { getByText } = render(
wrapInTestApp(
<Ingresses ingresses={(ingressesFixture as any).default} />,
),
);
// title
expect(getByText('dice-roller')).toBeInTheDocument();
expect(getByText('Ingress')).toBeInTheDocument();
// values
expect(getByText('Backend')).toBeInTheDocument();
expect(getByText('Ip: 192.168.64.2')).toBeInTheDocument();
expect(getByText('Rules')).toBeInTheDocument();
expect(getByText('Host: nginx')).toBeInTheDocument();
expect(getByText('Http:')).toBeInTheDocument();
expect(getByText('Paths:')).toBeInTheDocument();
expect(getByText('Service Name: dice-roller')).toBeInTheDocument();
expect(getByText('Service Port: 80')).toBeInTheDocument();
expect(getByText('Path: /')).toBeInTheDocument();
expect(getByText('Path Type: ImplementationSpecific')).toBeInTheDocument();
});
});
@@ -0,0 +1,51 @@
/*
* 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 React from 'react';
import { Grid } from '@material-ui/core';
import { ExtensionsV1beta1Ingress } from '@kubernetes/client-node';
import { InfoCard, StructuredMetadataTable } from '@backstage/core';
type IngressesProps = {
ingresses: ExtensionsV1beta1Ingress[];
children?: React.ReactNode;
};
export const Ingresses = ({ ingresses }: IngressesProps) => {
return (
<Grid container>
{ingresses.map((ingress, i) => {
return (
<Grid item key={i}>
<InfoCard
title={ingress.metadata?.name ?? 'un-named ingress'}
subheader="Ingress"
>
<div>
<StructuredMetadataTable
metadata={{
backend: ingress.status?.loadBalancer?.ingress,
rules: ingress.spec?.rules,
}}
/>
</div>
</InfoCard>
</Grid>
);
})}
</Grid>
);
};
@@ -0,0 +1,87 @@
[
{
"metadata": {
"annotations": {
"kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"networking.k8s.io/v1beta1\",\"kind\":\"Ingress\",\"metadata\":{\"annotations\":{\"nginx.ingress.kubernetes.io/rewrite-target\":\"/$1\"},\"labels\":{\"backstage.io/kubernetes-id\":\"dice-roller\"},\"name\":\"dice-roller\",\"namespace\":\"default\"},\"spec\":{\"rules\":[{\"host\":\"nginx\",\"http\":{\"paths\":[{\"backend\":{\"serviceName\":\"dice-roller\",\"servicePort\":80},\"path\":\"/\"}]}}]}}\n",
"nginx.ingress.kubernetes.io/rewrite-target": "/$1"
},
"creationTimestamp": "2020-09-28T13:28:00.000Z",
"generation": 1,
"labels": {
"backstage.io/kubernetes-id": "dice-roller"
},
"managedFields": [
{
"apiVersion": "networking.k8s.io/v1beta1",
"fieldsType": "FieldsV1",
"fieldsV1": {
"f:metadata": {
"f:annotations": {
".": {},
"f:kubectl.kubernetes.io/last-applied-configuration": {},
"f:nginx.ingress.kubernetes.io/rewrite-target": {}
},
"f:labels": {
".": {},
"f:backstage.io/kubernetes-id": {}
}
},
"f:spec": {
"f:rules": {}
}
},
"manager": "kubectl",
"operation": "Update",
"time": "2020-09-28T13:28:21.000Z"
},
{
"apiVersion": "networking.k8s.io/v1beta1",
"fieldsType": "FieldsV1",
"fieldsV1": {
"f:status": {
"f:loadBalancer": {
"f:ingress": {}
}
}
},
"manager": "nginx-ingress-controller",
"operation": "Update",
"time": "2020-09-28T13:28:40.000Z"
}
],
"name": "dice-roller",
"namespace": "default",
"resourceVersion": "699017",
"selfLink": "/apis/networking.k8s.io/v1beta1/namespaces/default/ingresses/dice-roller",
"uid": "e96994c0-49b9-4c1c-8ce0-72c5336fe960"
},
"spec": {
"rules": [
{
"host": "nginx",
"http": {
"paths": [
{
"backend": {
"serviceName": "dice-roller",
"servicePort": 80
},
"path": "/",
"pathType": "ImplementationSpecific"
}
]
}
}
]
},
"status": {
"loadBalancer": {
"ingress": [
{
"ip": "192.168.64.2"
}
]
}
}
}
]
@@ -0,0 +1,16 @@
/*
* 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 { Ingresses } from './Ingresses';
@@ -34,15 +34,25 @@ import {
} from '@backstage/plugin-kubernetes-backend';
import { DeploymentTables } from '../DeploymentTables';
import { DeploymentTriple } from '../../types/types';
import { V1ConfigMap, V1Service } from '@kubernetes/client-node';
import {
ExtensionsV1beta1Ingress,
V1ConfigMap,
V1HorizontalPodAutoscaler,
V1Service,
} from '@kubernetes/client-node';
import { Services } from '../Services';
import { ConfigMaps } from '../ConfigMaps';
import { Ingresses } from '../Ingresses';
import { HorizontalPodAutoscalers } from '../HorizontalPodAutoscalers';
interface GroupedResponses extends DeploymentTriple {
services: V1Service[];
configMaps: V1ConfigMap[];
horizontalPodAutoscalers: V1HorizontalPodAutoscaler[];
ingresses: ExtensionsV1beta1Ingress[];
}
// TODO this could probably be a lodash groupBy
const groupResponses = (fetchResponse: FetchResponse[]) => {
return fetchResponse.reduce(
(prev, next) => {
@@ -62,6 +72,12 @@ const groupResponses = (fetchResponse: FetchResponse[]) => {
case 'configmaps':
prev.configMaps.push(...next.resources);
break;
case 'horizontalpodautoscalers':
prev.horizontalPodAutoscalers.push(...next.resources);
break;
case 'ingresses':
prev.ingresses.push(...next.resources);
break;
default:
}
return prev;
@@ -72,6 +88,8 @@ const groupResponses = (fetchResponse: FetchResponse[]) => {
deployments: [],
services: [],
configMaps: [],
horizontalPodAutoscalers: [],
ingresses: [],
} as GroupedResponses,
);
};
@@ -130,6 +148,8 @@ const Cluster = ({ clusterObjects }: ClusterProps) => {
const groupedResponses = groupResponses(clusterObjects.resources);
const configMaps = groupedResponses.configMaps;
const hpas = groupedResponses.horizontalPodAutoscalers;
const ingresses = groupedResponses.ingresses;
return (
<>
@@ -151,10 +171,20 @@ const Cluster = ({ clusterObjects }: ClusterProps) => {
<Services services={groupedResponses.services} />
</CardTab>
{configMaps && (
<CardTab value="three" label="ConfigMaps">
<CardTab value="three" label="Config Maps">
<ConfigMaps configMaps={configMaps} />
</CardTab>
)}
{hpas && (
<CardTab value="four" label="Horizontal Pod Autoscalers">
<HorizontalPodAutoscalers hpas={hpas} />
</CardTab>
)}
{ingresses && (
<CardTab value="five" label="Ingresses">
<Ingresses ingresses={ingresses} />
</CardTab>
)}
</TabbedCard>
</>
);
+20
View File
@@ -0,0 +1,20 @@
/*
* 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.
*/
// Display the value unknown rather than 'undefined'
export function orUnknown<T>(val: T | undefined): T | string {
return val ?? 'unknown';
}