k8s add hpa and ingresses UI (#2640)
* k8s add hpa and ingresses UI * add todo
This commit is contained in:
@@ -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 (
|
||||
|
||||
@@ -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
|
||||
|
||||
+52
@@ -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);
|
||||
});
|
||||
});
|
||||
+64
@@ -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>
|
||||
);
|
||||
};
|
||||
+81
@@ -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>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
Reference in New Issue
Block a user