From d66c6a7dcbf5bc682b248d08be5ddb938044b562 Mon Sep 17 00:00:00 2001 From: Matthew Clarke Date: Mon, 28 Sep 2020 19:32:35 +0100 Subject: [PATCH] k8s add hpa and ingresses UI (#2640) * k8s add hpa and ingresses UI * add todo --- .../examples/dice-roller/README.md | 4 +- .../dice-roller/dice-roller-manifests.yaml | 35 ++++++++ .../src/service/KubernetesClientProvider.ts | 50 ++++++++++- .../src/service/KubernetesFetcher.test.ts | 2 + .../src/service/KubernetesFetcher.ts | 75 +++++++++++----- .../getKubernetesObjectsByServiceIdHandler.ts | 2 + plugins/kubernetes-backend/src/types/types.ts | 28 +++--- .../components/ConfigMaps/ConfigMaps.test.tsx | 2 +- .../src/components/ConfigMaps/ConfigMaps.tsx | 2 +- .../HorizontalPodAutoscalers.test.tsx | 52 +++++++++++ .../HorizontalPodAutoscalers.tsx | 64 ++++++++++++++ .../horizontalpodautoscalers.json | 81 +++++++++++++++++ .../HorizontalPodAutoscalers/index.ts | 16 ++++ .../components/Ingresses/Ingresses.test.tsx | 47 ++++++++++ .../src/components/Ingresses/Ingresses.tsx | 51 +++++++++++ .../Ingresses/__fixtures__/ingress.json | 87 +++++++++++++++++++ .../src/components/Ingresses/index.ts | 16 ++++ .../KubernetesContent/KubernetesContent.tsx | 34 +++++++- plugins/kubernetes/src/utils.ts | 20 +++++ 19 files changed, 632 insertions(+), 36 deletions(-) create mode 100644 plugins/kubernetes/src/components/HorizontalPodAutoscalers/HorizontalPodAutoscalers.test.tsx create mode 100644 plugins/kubernetes/src/components/HorizontalPodAutoscalers/HorizontalPodAutoscalers.tsx create mode 100644 plugins/kubernetes/src/components/HorizontalPodAutoscalers/__fixtures__/horizontalpodautoscalers.json create mode 100644 plugins/kubernetes/src/components/HorizontalPodAutoscalers/index.ts create mode 100644 plugins/kubernetes/src/components/Ingresses/Ingresses.test.tsx create mode 100644 plugins/kubernetes/src/components/Ingresses/Ingresses.tsx create mode 100644 plugins/kubernetes/src/components/Ingresses/__fixtures__/ingress.json create mode 100644 plugins/kubernetes/src/components/Ingresses/index.ts create mode 100644 plugins/kubernetes/src/utils.ts diff --git a/plugins/kubernetes-backend/examples/dice-roller/README.md b/plugins/kubernetes-backend/examples/dice-roller/README.md index b64e2df37f..47fc345c73 100644 --- a/plugins/kubernetes-backend/examples/dice-roller/README.md +++ b/plugins/kubernetes-backend/examples/dice-roller/README.md @@ -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 diff --git a/plugins/kubernetes-backend/examples/dice-roller/dice-roller-manifests.yaml b/plugins/kubernetes-backend/examples/dice-roller/dice-roller-manifests.yaml index 13fe26d051..50956bd7d3 100644 --- a/plugins/kubernetes-backend/examples/dice-roller/dice-roller-manifests.yaml +++ b/plugins/kubernetes-backend/examples/dice-roller/dice-roller-manifests.yaml @@ -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 diff --git a/plugins/kubernetes-backend/src/service/KubernetesClientProvider.ts b/plugins/kubernetes-backend/src/service/KubernetesClientProvider.ts index c3986e3ff7..8b12d53afc 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesClientProvider.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesClientProvider.ts @@ -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; + } } diff --git a/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts b/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts index 359e7c5806..0109b12f36 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts @@ -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({ diff --git a/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts b/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts index f6d3edb5e9..8890db7b13 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts @@ -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> { - return this.singleClusterFetch(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> { + return this.singleClusterFetch( + clusterDetails, + ({ autoscaling }) => + autoscaling.listHorizontalPodAutoscalerForAllNamespaces( + false, + '', + '', + `backstage.io/kubernetes-id=${serviceId}`, + ), + ); + } + + private fetchIngressesByServiceId( + serviceId: string, + clusterDetails: ClusterDetails, + ): Promise> { + return this.singleClusterFetch( + clusterDetails, + ({ networkingBeta1 }) => + networkingBeta1.listIngressForAllNamespaces( + false, + '', + '', + `backstage.io/kubernetes-id=${serviceId}`, + ), + ); + } } diff --git a/plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.ts b/plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.ts index b2dde1a393..552e083bd7 100644 --- a/plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.ts +++ b/plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.ts @@ -36,6 +36,8 @@ const DEFAULT_OBJECTS = new Set([ 'configmaps', 'deployments', 'replicasets', + 'horizontalpodautoscalers', + 'ingresses', ]); export const handleGetKubernetesObjectsByServiceId: GetKubernetesObjectsByServiceIdHandler = async ( diff --git a/plugins/kubernetes-backend/src/types/types.ts b/plugins/kubernetes-backend/src/types/types.ts index 2c69fd67a0..e469b2b1b5 100644 --- a/plugins/kubernetes-backend/src/types/types.ts +++ b/plugins/kubernetes-backend/src/types/types.ts @@ -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; } -export interface SecretFetchResponse { - type: 'secrets'; - resources: Array; -} - export interface DeploymentFetchResponse { type: 'deployments'; resources: Array; @@ -87,6 +85,16 @@ export interface ReplicaSetsFetchResponse { resources: Array; } +export interface HorizontalPodAutoscalersFetchResponse { + type: 'horizontalpodautoscalers'; + resources: Array; +} + +export interface IngressesFetchResponse { + type: 'ingresses'; + resources: Array; +} + // Fetches information from a kubernetes cluster using the cluster details object // to target a specific cluster export interface KubernetesFetcher { diff --git a/plugins/kubernetes/src/components/ConfigMaps/ConfigMaps.test.tsx b/plugins/kubernetes/src/components/ConfigMaps/ConfigMaps.test.tsx index 161a0ed487..35b8f2143a 100644 --- a/plugins/kubernetes/src/components/ConfigMaps/ConfigMaps.test.tsx +++ b/plugins/kubernetes/src/components/ConfigMaps/ConfigMaps.test.tsx @@ -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(); diff --git a/plugins/kubernetes/src/components/ConfigMaps/ConfigMaps.tsx b/plugins/kubernetes/src/components/ConfigMaps/ConfigMaps.tsx index f757d1c0fc..5a013eb2c6 100644 --- a/plugins/kubernetes/src/components/ConfigMaps/ConfigMaps.tsx +++ b/plugins/kubernetes/src/components/ConfigMaps/ConfigMaps.tsx @@ -32,7 +32,7 @@ export const ConfigMaps = ({ configMaps }: ConfigMapsProps) => {
{ + it('should render horizontalpodautoscaler', async () => { + const { getByText, getAllByText } = render( + wrapInTestApp( + , + ), + ); + + // 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); + }); +}); diff --git a/plugins/kubernetes/src/components/HorizontalPodAutoscalers/HorizontalPodAutoscalers.tsx b/plugins/kubernetes/src/components/HorizontalPodAutoscalers/HorizontalPodAutoscalers.tsx new file mode 100644 index 0000000000..b4540840d2 --- /dev/null +++ b/plugins/kubernetes/src/components/HorizontalPodAutoscalers/HorizontalPodAutoscalers.tsx @@ -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 ( + + {hpas.map((hpa, i) => { + return ( + + +
+ +
+
+
+ ); + })} +
+ ); +}; diff --git a/plugins/kubernetes/src/components/HorizontalPodAutoscalers/__fixtures__/horizontalpodautoscalers.json b/plugins/kubernetes/src/components/HorizontalPodAutoscalers/__fixtures__/horizontalpodautoscalers.json new file mode 100644 index 0000000000..6bc6028246 --- /dev/null +++ b/plugins/kubernetes/src/components/HorizontalPodAutoscalers/__fixtures__/horizontalpodautoscalers.json @@ -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 + } + } +] diff --git a/plugins/kubernetes/src/components/HorizontalPodAutoscalers/index.ts b/plugins/kubernetes/src/components/HorizontalPodAutoscalers/index.ts new file mode 100644 index 0000000000..3eebbe9813 --- /dev/null +++ b/plugins/kubernetes/src/components/HorizontalPodAutoscalers/index.ts @@ -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'; diff --git a/plugins/kubernetes/src/components/Ingresses/Ingresses.test.tsx b/plugins/kubernetes/src/components/Ingresses/Ingresses.test.tsx new file mode 100644 index 0000000000..f2507a3f39 --- /dev/null +++ b/plugins/kubernetes/src/components/Ingresses/Ingresses.test.tsx @@ -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( + , + ), + ); + + // 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(); + }); +}); diff --git a/plugins/kubernetes/src/components/Ingresses/Ingresses.tsx b/plugins/kubernetes/src/components/Ingresses/Ingresses.tsx new file mode 100644 index 0000000000..37932601a1 --- /dev/null +++ b/plugins/kubernetes/src/components/Ingresses/Ingresses.tsx @@ -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 ( + + {ingresses.map((ingress, i) => { + return ( + + +
+ +
+
+
+ ); + })} +
+ ); +}; diff --git a/plugins/kubernetes/src/components/Ingresses/__fixtures__/ingress.json b/plugins/kubernetes/src/components/Ingresses/__fixtures__/ingress.json new file mode 100644 index 0000000000..fd0bc5ec43 --- /dev/null +++ b/plugins/kubernetes/src/components/Ingresses/__fixtures__/ingress.json @@ -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" + } + ] + } + } + } +] diff --git a/plugins/kubernetes/src/components/Ingresses/index.ts b/plugins/kubernetes/src/components/Ingresses/index.ts new file mode 100644 index 0000000000..ecc67480db --- /dev/null +++ b/plugins/kubernetes/src/components/Ingresses/index.ts @@ -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'; diff --git a/plugins/kubernetes/src/components/KubernetesContent/KubernetesContent.tsx b/plugins/kubernetes/src/components/KubernetesContent/KubernetesContent.tsx index 315a5e0377..b926d92dc7 100644 --- a/plugins/kubernetes/src/components/KubernetesContent/KubernetesContent.tsx +++ b/plugins/kubernetes/src/components/KubernetesContent/KubernetesContent.tsx @@ -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) => { {configMaps && ( - + )} + {hpas && ( + + + + )} + {ingresses && ( + + + + )} ); diff --git a/plugins/kubernetes/src/utils.ts b/plugins/kubernetes/src/utils.ts new file mode 100644 index 0000000000..e038b1af8b --- /dev/null +++ b/plugins/kubernetes/src/utils.ts @@ -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(val: T | undefined): T | string { + return val ?? 'unknown'; +}