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
@@ -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';
}