From 5f416921e95147b557545c9223122ed44769c92e Mon Sep 17 00:00:00 2001 From: Alex Andreev Date: Mon, 23 Jan 2023 16:22:44 +0300 Subject: [PATCH] Parse HPA metrics from different versions (#6971) * First sketch of hpav2 metrics Signed-off-by: Alex Andreev * HPA metrics initial tests Signed-off-by: Alex Andreev * Receive target Object metrics for v2 Signed-off-by: Alex Andreev * External target metrics Signed-off-by: Alex Andreev * Add more types for ObjectMetricStatus Signed-off-by: Alex Andreev * Move metrics parsing from HPA object to separate injectable Signed-off-by: Alex Andreev * Add metric parser for HPA v2 Signed-off-by: Alex Andreev * Using metrics parser in hpa list and details Signed-off-by: Alex Andreev * Add more test cases for HPA v2 Signed-off-by: Alex Andreev * Add HorizontalPodAutoscalerV1MetricParser Signed-off-by: Alex Andreev * Adding injectable for hpa v1 metric parser Signed-off-by: Alex Andreev * Adding test cases for autoscaling/v1 metrics Signed-off-by: Alex Andreev * Add test cases for hpa beta versions Signed-off-by: Alex Andreev * Check for legacy targetCPUUtilizationPercentage Signed-off-by: Alex Andreev * Fix external metirc parser output Signed-off-by: Alex Andreev * Small clean up Signed-off-by: Alex Andreev * Linter fixes Signed-off-by: Alex Andreev * Removing fallbackApiBases Signed-off-by: Alex Andreev * Remove left comments Signed-off-by: Alex Andreev * Making metric parser classes as not injectable Signed-off-by: Alex Andreev * Fix metrics in hpa details for newer versions Signed-off-by: Alex Andreev * Spreading types to V2 and V2Beta1 Signed-off-by: Alex Andreev * Move getMetricName() to its own file Signed-off-by: Alex Andreev Signed-off-by: Alex Andreev --- .../horizontal-pod-autoscaler.api.ts | 339 ++--- .../get-hpa-metric-name.ts | 37 + .../get-hpa-metrics.injectable.ts | 66 + .../horizontal-pod-autoscaler-metrics.test.ts | 1102 +++++++++++++++++ .../+config-autoscalers/hpa-details.scss | 10 + .../+config-autoscalers/hpa-details.tsx | 29 +- .../hpa-v1-metric-parser.ts | 70 ++ .../hpa-v2-metric-parser.ts | 69 ++ .../components/+config-autoscalers/hpa.tsx | 7 +- ...l-pod-autoscaler-detail-item.injectable.ts | 2 +- 10 files changed, 1564 insertions(+), 167 deletions(-) create mode 100644 src/renderer/components/+config-autoscalers/get-hpa-metric-name.ts create mode 100644 src/renderer/components/+config-autoscalers/get-hpa-metrics.injectable.ts create mode 100644 src/renderer/components/+config-autoscalers/horizontal-pod-autoscaler-metrics.test.ts create mode 100644 src/renderer/components/+config-autoscalers/hpa-v1-metric-parser.ts create mode 100644 src/renderer/components/+config-autoscalers/hpa-v2-metric-parser.ts diff --git a/src/common/k8s-api/endpoints/horizontal-pod-autoscaler.api.ts b/src/common/k8s-api/endpoints/horizontal-pod-autoscaler.api.ts index ee4fcfc63d..ba5483da2c 100644 --- a/src/common/k8s-api/endpoints/horizontal-pod-autoscaler.api.ts +++ b/src/common/k8s-api/endpoints/horizontal-pod-autoscaler.api.ts @@ -3,11 +3,11 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import type { BaseKubeObjectCondition, LabelSelector, NamespaceScopedMetadata } from "../kube-object"; -import { KubeObject } from "../kube-object"; +import type { OptionVarient } from "../../utils"; import type { DerivedKubeApiOptions, KubeApiDependencies } from "../kube-api"; import { KubeApi } from "../kube-api"; -import type { OptionVarient } from "../../utils"; +import type { BaseKubeObjectCondition, LabelSelector, NamespaceScopedMetadata } from "../kube-object"; +import { KubeObject } from "../kube-object"; export enum HpaMetricType { Resource = "Resource", @@ -17,46 +17,131 @@ export enum HpaMetricType { ContainerResource = "ContainerResource", } +export interface MetricCurrentTarget { + current?: string; + target?: string; +} + export interface HorizontalPodAutoscalerMetricTarget { kind: string; name: string; apiVersion: string; } -export interface ContainerResourceMetricSource { +export interface V2ContainerResourceMetricSource { + container: string; + name: string; + target?: { + averageUtilization?: number; + averageValue?: string; + type?: string; + }; +} + +export interface V2Beta1ContainerResourceMetricSource { container: string; name: string; targetAverageUtilization?: number; targetAverageValue?: string; } -export interface ExternalMetricSource { - metricName: string; +export type ContainerResourceMetricSource = + | V2ContainerResourceMetricSource + | V2Beta1ContainerResourceMetricSource; + +export interface V2ExternalMetricSource { + metricName?: string; + metricSelector?: LabelSelector; + metric?: { + name?: string; + selector?: LabelSelector; + }; + target?: { + type: string; + value?: string; + averageValue?: string; + }; +} + +export interface V2Beta1ExternalMetricSource { + metricName?: string; metricSelector?: LabelSelector; targetAverageValue?: string; targetValue?: string; + metric?: { + selector?: LabelSelector; + }; } -export interface ObjectMetricSource { +export type ExternalMetricSource = + | V2Beta1ExternalMetricSource + | V2ExternalMetricSource; + +export interface V2ObjectMetricSource { + metric?: { + name?: string; + selector?: LabelSelector; + }; + target?: { + type?: string; + value?: string; + averageValue?: string; + }; + describedObject?: CrossVersionObjectReference; +} + +export interface V2Beta1ObjectMetricSource { averageValue?: string; - metricName: string; + metricName?: string; selector?: LabelSelector; - target: CrossVersionObjectReference; - targetValue: string; + targetValue?: string; + describedObject?: CrossVersionObjectReference; } -export interface PodsMetricSource { - metricName: string; - selector?: LabelSelector; - targetAverageValue: string; +export type ObjectMetricSource = + | V2ObjectMetricSource + | V2Beta1ObjectMetricSource; + +export interface V2PodsMetricSource { + metric?: { + name?: string; + selector?: LabelSelector; + }; + target?: { + averageValue?: string; + type?: string; + }; } -export interface ResourceMetricSource { +export interface V2Beta1PodsMetricSource { + metricName?: string; + selector?: LabelSelector; + targetAverageValue?: string; +} + +export type PodsMetricSource = + | V2PodsMetricSource + | V2Beta1PodsMetricSource; + +export interface V2ResourceMetricSource { + name: string; + target?: { + averageUtilization?: number; + averageValue?: string; + type?: string; + }; +} + +export interface V2Beta1ResourceMetricSource { name: string; targetAverageUtilization?: number; targetAverageValue?: string; } +export type ResourceMetricSource = + | V2ResourceMetricSource + | V2Beta1ResourceMetricSource; + export interface BaseHorizontalPodAutoscalerMetricSpec { containerResource: ContainerResourceMetricSource; external: ExternalMetricSource; @@ -93,40 +178,112 @@ interface HPAScalingPolicy { type HPAScalingPolicyType = string; -export interface ContainerResourceMetricStatus { - container: string; +export interface V2ContainerResourceMetricStatus { + container?: string; + name: string; + current?: { + averageUtilization?: number; + averageValue?: string; + }; +} + +export interface V2Beta1ContainerResourceMetricStatus { + container?: string; currentAverageUtilization?: number; - currentAverageValue: string; + currentAverageValue?: string; name: string; } -export interface ExternalMetricStatus { +export type ContainerResourceMetricStatus = + | V2ContainerResourceMetricStatus + | V2Beta1ContainerResourceMetricStatus; + +export interface V2ExternalMetricStatus { + metric?: { + name?: string; + selector?: LabelSelector; + }; + current?: { + averageValue?: string; + value?: string; + }; +} + +export interface V2Beta1ExternalMetricStatus { currentAverageValue?: string; - currentValue: string; - metricName: string; + currentValue?: string; + metricName?: string; metricSelector?: LabelSelector; } -export interface ObjectMetricStatus { +export type ExternalMetricStatus = + | V2Beta1ExternalMetricStatus + | V2ExternalMetricStatus; + +export interface V2ObjectMetricStatus { + metric?: { + name?: string; + selector?: LabelSelector; + }; + current?: { + type?: string; + value?: string; + averageValue?: string; + }; + describedObject?: CrossVersionObjectReference; +} + +export interface V2Beta1ObjectMetricStatus { averageValue?: string; currentValue?: string; - metricName: string; + metricName?: string; selector?: LabelSelector; - target: CrossVersionObjectReference; + describedObject?: CrossVersionObjectReference; } -export interface PodsMetricStatus { - currentAverageValue: string; - metricName: string; +export type ObjectMetricStatus = + | V2Beta1ObjectMetricStatus + | V2ObjectMetricStatus; + +export interface V2PodsMetricStatus { + selector?: LabelSelector; + metric?: { + name?: string; + selector?: LabelSelector; + }; + current?: { + averageValue?: string; + }; +} + +export interface V2Beta1PodsMetricStatus { + currentAverageValue?: string; + metricName?: string; selector?: LabelSelector; } -export interface ResourceMetricStatus { +export type PodsMetricStatus = + | V2Beta1PodsMetricStatus + | V2PodsMetricStatus; + +export interface V2ResourceMetricStatus { + name: string; + current?: { + averageUtilization?: number; + averageValue?: string; + }; +} + +export interface V2Beta1ResourceMetricStatus { currentAverageUtilization?: number; - currentAverageValue: string; + currentAverageValue?: string; name: string; } +export type ResourceMetricStatus = + | V2Beta1ResourceMetricStatus + | V2ResourceMetricStatus; + export interface BaseHorizontalPodAutoscalerMetricStatus { containerResource: ContainerResourceMetricStatus; external: ExternalMetricStatus; @@ -154,6 +311,7 @@ export interface HorizontalPodAutoscalerSpec { maxReplicas: number; metrics?: HorizontalPodAutoscalerMetricSpec[]; behavior?: HorizontalPodAutoscalerBehavior; + targetCPUUtilizationPercentage?: number; // used only in autoscaling/v1 } export interface HorizontalPodAutoscalerStatus { @@ -161,11 +319,7 @@ export interface HorizontalPodAutoscalerStatus { currentReplicas: number; desiredReplicas: number; currentMetrics?: HorizontalPodAutoscalerMetricStatus[]; -} - -interface MetricCurrentTarget { - current?: string; - target?: string; + currentCPUUtilizationPercentage?: number; // used only in autoscaling/v1 } export class HorizontalPodAutoscaler extends KubeObject< @@ -212,15 +366,6 @@ export class HorizontalPodAutoscaler extends KubeObject< getCurrentMetrics() { return this.status?.currentMetrics ?? []; } - - getMetricValues(metric: HorizontalPodAutoscalerMetricSpec): string { - const { - current = "unknown", - target = "unknown", - } = getMetricCurrentTarget(metric, this.getCurrentMetrics()); - - return `${current} / ${target}`; - } } export class HorizontalPodAutoscalerApi extends KubeApi { @@ -229,114 +374,6 @@ export class HorizontalPodAutoscalerApi extends KubeApi ...opts ?? {}, objectConstructor: HorizontalPodAutoscaler, checkPreferredVersion: true, - // Kubernetes < 1.26 - fallbackApiBases: [ - "/apis/autoscaling/v2beta2/horizontalpodautoscalers", - "/apis/autoscaling/v2beta1/horizontalpodautoscalers", - "/apis/autoscaling/v1/horizontalpodautoscalers", - ], }); } } - -function getMetricName(metric: HorizontalPodAutoscalerMetricSpec | HorizontalPodAutoscalerMetricStatus): string | undefined { - switch (metric.type) { - case HpaMetricType.Resource: - return metric.resource.name; - case HpaMetricType.Pods: - return metric.pods.metricName; - case HpaMetricType.Object: - return metric.object.metricName; - case HpaMetricType.External: - return metric.external.metricName; - case HpaMetricType.ContainerResource: - return metric.containerResource.name; - default: - return undefined; - } -} - -function getResourceMetricValue(currentMetric: ResourceMetricStatus | undefined, targetMetric: ResourceMetricSource): MetricCurrentTarget { - return { - current: ( - typeof currentMetric?.currentAverageUtilization === "number" - ? `${currentMetric.currentAverageUtilization}%` - : currentMetric?.currentAverageValue - ), - target: ( - typeof targetMetric?.targetAverageUtilization === "number" - ? `${targetMetric.targetAverageUtilization}%` - : targetMetric?.targetAverageValue - ), - }; -} - -function getPodsMetricValue(currentMetric: PodsMetricStatus | undefined, targetMetric: PodsMetricSource): MetricCurrentTarget { - return { - current: currentMetric?.currentAverageValue, - target: targetMetric?.targetAverageValue, - }; -} - -function getObjectMetricValue(currentMetric: ObjectMetricStatus | undefined, targetMetric: ObjectMetricSource): MetricCurrentTarget { - return { - current: ( - currentMetric?.currentValue - ?? currentMetric?.averageValue - ), - target: ( - targetMetric?.targetValue - ?? targetMetric?.averageValue - ), - }; -} - -function getExternalMetricValue(currentMetric: ExternalMetricStatus | undefined, targetMetric: ExternalMetricSource): MetricCurrentTarget { - return { - current: ( - currentMetric?.currentValue - ?? currentMetric?.currentAverageValue - ), - target: ( - targetMetric?.targetValue - ?? targetMetric?.targetAverageValue - ), - }; -} - -function getContainerResourceMetricValue(currentMetric: ContainerResourceMetricStatus | undefined, targetMetric: ContainerResourceMetricSource): MetricCurrentTarget { - return { - current: ( - typeof currentMetric?.currentAverageUtilization === "number" - ? `${currentMetric.currentAverageUtilization}%` - : currentMetric?.currentAverageValue - ), - target: ( - typeof targetMetric?.targetAverageUtilization === "number" - ? `${targetMetric.targetAverageUtilization}%` - : targetMetric?.targetAverageValue - ), - }; -} - -function getMetricCurrentTarget(spec: HorizontalPodAutoscalerMetricSpec, status: HorizontalPodAutoscalerMetricStatus[]): MetricCurrentTarget { - const currentMetric = status.find(m => ( - m.type === spec.type - && getMetricName(m) === getMetricName(spec) - )); - - switch (spec.type) { - case HpaMetricType.Resource: - return getResourceMetricValue(currentMetric?.resource, spec.resource); - case HpaMetricType.Pods: - return getPodsMetricValue(currentMetric?.pods, spec.pods); - case HpaMetricType.Object: - return getObjectMetricValue(currentMetric?.object, spec.object); - case HpaMetricType.External: - return getExternalMetricValue(currentMetric?.external, spec.external); - case HpaMetricType.ContainerResource: - return getContainerResourceMetricValue(currentMetric?.containerResource, spec.containerResource); - default: - return {}; - } -} diff --git a/src/renderer/components/+config-autoscalers/get-hpa-metric-name.ts b/src/renderer/components/+config-autoscalers/get-hpa-metric-name.ts new file mode 100644 index 0000000000..f92b249909 --- /dev/null +++ b/src/renderer/components/+config-autoscalers/get-hpa-metric-name.ts @@ -0,0 +1,37 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { HpaMetricType } from "../../../common/k8s-api/endpoints"; +import type { LabelSelector } from "../../../common/k8s-api/kube-object"; + +type MetricNames = Partial>; + +interface Metric extends MetricNames { + type: HpaMetricType; +} + +export function getMetricName(metric: Metric): string | undefined { + switch (metric.type) { + case HpaMetricType.Resource: + return metric.resource?.name; + case HpaMetricType.Pods: + return metric.pods?.metricName || metric.pods?.metric?.name; + case HpaMetricType.Object: + return metric.object?.metricName || metric.object?.metric?.name; + case HpaMetricType.External: + return metric.external?.metricName || metric.external?.metric?.name; + case HpaMetricType.ContainerResource: + return metric.containerResource?.name; + default: + return undefined; + } +} diff --git a/src/renderer/components/+config-autoscalers/get-hpa-metrics.injectable.ts b/src/renderer/components/+config-autoscalers/get-hpa-metrics.injectable.ts new file mode 100644 index 0000000000..ac524f0cd5 --- /dev/null +++ b/src/renderer/components/+config-autoscalers/get-hpa-metrics.injectable.ts @@ -0,0 +1,66 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import type { HorizontalPodAutoscaler, HorizontalPodAutoscalerMetricSpec, HorizontalPodAutoscalerMetricStatus } from "../../../common/k8s-api/endpoints"; +import { HpaMetricType } from "../../../common/k8s-api/endpoints"; +import { getMetricName } from "./get-hpa-metric-name"; +import { HorizontalPodAutoscalerV1MetricParser } from "./hpa-v1-metric-parser"; +import { HorizontalPodAutoscalerV2MetricParser } from "./hpa-v2-metric-parser"; + +type Parser = HorizontalPodAutoscalerV1MetricParser | HorizontalPodAutoscalerV2MetricParser; + +const getHorizontalPodAutoscalerMetrics = getInjectable({ + id: "get-horizontal-pod-autoscaler-metrics", + instantiate: () => (hpa: HorizontalPodAutoscaler) => { + const hpaV1Parser = new HorizontalPodAutoscalerV1MetricParser(); + const hpaV2Parser = new HorizontalPodAutoscalerV2MetricParser(); + const metrics = hpa.spec?.metrics ?? []; + const currentMetrics = hpa.status?.currentMetrics ?? []; + const cpuUtilization = hpa.spec?.targetCPUUtilizationPercentage; + + if (cpuUtilization) { + const utilizationCurrent = hpa.status?.currentCPUUtilizationPercentage ? `${hpa.status.currentCPUUtilizationPercentage}%` : "unknown"; + const utilizationTarget = cpuUtilization ? `${cpuUtilization}%` : "unknown"; + + return [`${utilizationCurrent} / ${utilizationTarget}`]; + } + + return metrics.map((metric) => { + const currentMetric = currentMetrics.find(current => + current.type === metric.type + && getMetricName(current) === getMetricName(metric), + ); + + const h2Values = getMetricValues(hpaV2Parser, currentMetric, metric); + const h1Values = getMetricValues(hpaV1Parser, currentMetric, metric); + let values = h1Values; + + if (h2Values.current || h2Values.target) { + values = h2Values; + } + + return `${values.current ?? "unknown"} / ${values.target ?? "unknown"}`; + }); + }, +}); + +function getMetricValues(parser: Type, current: HorizontalPodAutoscalerMetricStatus | undefined, target: HorizontalPodAutoscalerMetricSpec) { + switch (target.type) { + case HpaMetricType.Resource: + return parser.getResource({ current: current?.resource, target: target.resource }); + case HpaMetricType.Pods: + return parser.getPods({ current: current?.pods, target: target.pods }); + case HpaMetricType.Object: + return parser.getObject({ current: current?.object, target: target.object }); + case HpaMetricType.External: + return parser.getExternal({ current: current?.external, target: target.external }); + case HpaMetricType.ContainerResource: + return parser.getContainerResource({ current: current?.containerResource, target: target.containerResource }); + default: + return {}; + } +} + +export default getHorizontalPodAutoscalerMetrics; diff --git a/src/renderer/components/+config-autoscalers/horizontal-pod-autoscaler-metrics.test.ts b/src/renderer/components/+config-autoscalers/horizontal-pod-autoscaler-metrics.test.ts new file mode 100644 index 0000000000..5f1c1d7183 --- /dev/null +++ b/src/renderer/components/+config-autoscalers/horizontal-pod-autoscaler-metrics.test.ts @@ -0,0 +1,1102 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import type { DiContainer } from "@ogre-tools/injectable"; +import getHorizontalPodAutoscalerMetrics from "./get-hpa-metrics.injectable"; +import { getDiForUnitTesting } from "../../getDiForUnitTesting"; +import { HorizontalPodAutoscaler, HpaMetricType } from "../../../common/k8s-api/endpoints"; + +const hpaV2 = { + apiVersion: "autoscaling/v2", + kind: "HorizontalPodAutoscaler", + metadata: { + name: "hpav2", + resourceVersion: "1", + uid: "hpav2", + namespace: "default", + selfLink: "/apis/autoscaling/v2/namespaces/default/horizontalpodautoscalers/hpav2", + }, + spec: { + maxReplicas: 10, + scaleTargetRef: { + kind: "Deployment", + name: "hpav2deployment", + apiVersion: "apps/v1", + }, + }, +}; + +const hpaV2Beta1 = { + apiVersion: "autoscaling/v2beta1", + kind: "HorizontalPodAutoscaler", + metadata: { + name: "hpav2beta1", + resourceVersion: "1", + uid: "hpav1", + namespace: "default", + selfLink: "/apis/autoscaling/v2beta1/namespaces/default/horizontalpodautoscalers/hpav2beta1", + }, + spec: { + maxReplicas: 10, + scaleTargetRef: { + kind: "Deployment", + name: "hpav1deployment", + apiVersion: "apps/v1", + }, + }, +}; + +describe("getHorizontalPodAutoscalerMetrics", () => { + let di: DiContainer; + let getMetrics: (hpa: HorizontalPodAutoscaler) => string[]; + + beforeEach(() => { + di = getDiForUnitTesting(); + + getMetrics = di.inject(getHorizontalPodAutoscalerMetrics); + }); + + describe("HPA v2", () => { + it("should return correct empty metrics", () => { + const hpa = new HorizontalPodAutoscaler(hpaV2); + + expect(getMetrics(hpa)).toHaveLength(0); + }); + + it("should return correct resource metrics", () => { + const hpa = new HorizontalPodAutoscaler({ + ...hpaV2, + spec: { + ...hpaV2.spec, + metrics: [ + { + type: HpaMetricType.Resource, + resource: { + name: "cpu", + target: { + type: "Utilization", + averageUtilization: 50, + }, + }, + }, + ], + }, + }); + + expect(getMetrics(hpa)[0]).toEqual("unknown / 50%"); + }); + + it("should return correct resource metrics with current metrics", () => { + const hpa = new HorizontalPodAutoscaler({ + ...hpaV2, + spec: { + ...hpaV2.spec, + metrics: [ + { + type: HpaMetricType.Resource, + resource: { + name: "cpu", + target: { + type: "Utilization", + averageUtilization: 50, + }, + }, + }, + ], + }, + status: { + currentReplicas: 1, + desiredReplicas: 10, + currentMetrics: [ + { + type: HpaMetricType.Resource, + resource: { + name: "cpu", + current: { + averageValue: "100m", + averageUtilization: 10, + }, + }, + }, + ], + }, + }); + + expect(getMetrics(hpa)[0]).toEqual("10% / 50%"); + }); + + it("should return correct resource metrics with current value", () => { + const hpa = new HorizontalPodAutoscaler({ + ...hpaV2, + spec: { + ...hpaV2.spec, + metrics: [ + { + type: HpaMetricType.Resource, + resource: { + name: "cpu", + target: { + type: "Value", + averageValue: "100m", + }, + }, + }, + ], + }, + status: { + currentReplicas: 1, + desiredReplicas: 10, + currentMetrics: [ + { + type: HpaMetricType.Resource, + resource: { + name: "cpu", + current: { + averageValue: "500m", + }, + }, + }, + ], + }, + }); + + expect(getMetrics(hpa)[0]).toEqual("500m / 100m"); + }); + + it("should return correct container resource metrics", () => { + const hpa = new HorizontalPodAutoscaler( + { + ...hpaV2, + spec: { + ...hpaV2.spec, + metrics: [ + { + type: HpaMetricType.ContainerResource, + containerResource: { + name: "cpu", + container: "nginx", + target: { + type: "Utilization", + averageUtilization: 60, + }, + }, + }, + ], + }, + }, + ); + + expect(getMetrics(hpa)[0]).toEqual("unknown / 60%"); + }); + + it("should return correct container resource metrics with current utilization value", () => { + const hpa = new HorizontalPodAutoscaler( + { + ...hpaV2, + spec: { + ...hpaV2.spec, + metrics: [ + { + type: HpaMetricType.ContainerResource, + containerResource: { + name: "cpu", + container: "nginx", + target: { + type: "Utilization", + averageUtilization: 60, + }, + }, + }, + ], + }, + status: { + currentReplicas: 1, + desiredReplicas: 10, + currentMetrics: [ + { + type: HpaMetricType.ContainerResource, + containerResource: { + name: "cpu", + current: { + averageUtilization: 10, + }, + }, + }, + ], + }, + }, + ); + + expect(getMetrics(hpa)[0]).toEqual("10% / 60%"); + }); + + it("should return correct pod metrics", () => { + const hpa = new HorizontalPodAutoscaler( + { + ...hpaV2, + spec: { + ...hpaV2.spec, + metrics: [ + { + type: HpaMetricType.Pods, + pods: { + metric: { + name: "packets-per-second", + }, + target: { + type: "AverageValue", + averageValue: "1k", + }, + }, + }, + ], + }, + }, + ); + + expect(getMetrics(hpa)[0]).toEqual("unknown / 1k"); + }); + + it("should return correct pod metrics with current values", () => { + const hpa = new HorizontalPodAutoscaler( + { + ...hpaV2, + spec: { + ...hpaV2.spec, + metrics: [ + { + type: HpaMetricType.Pods, + pods: { + metric: { + name: "packets-per-second", + }, + target: { + type: "AverageValue", + averageValue: "1k", + }, + }, + }, + ], + }, + status: { + currentReplicas: 1, + desiredReplicas: 10, + currentMetrics: [ + { + type: HpaMetricType.Pods, + pods: { + metric: { + name: "packets-per-second", + }, + current: { + averageValue: "10", + }, + }, + }, + ], + }, + }, + ); + + expect(getMetrics(hpa)[0]).toEqual("10 / 1k"); + }); + + it("should return correct object metrics", () => { + const hpa = new HorizontalPodAutoscaler( + { + ...hpaV2, + spec: { + ...hpaV2.spec, + metrics: [ + { + type: HpaMetricType.Object, + object: { + metric: { + name: "requests-per-second", + }, + target: { + type: "Value", + value: "10k", + }, + }, + }, + ], + }, + }, + ); + + expect(getMetrics(hpa)[0]).toEqual("unknown / 10k"); + }); + + it("should return correct object metrics with average value", () => { + const hpa = new HorizontalPodAutoscaler( + { + ...hpaV2, + spec: { + ...hpaV2.spec, + metrics: [ + { + type: HpaMetricType.Object, + object: { + metric: { + name: "requests-per-second", + }, + target: { + type: "AverageValue", + averageValue: "5k", + }, + }, + }, + ], + }, + }, + ); + + expect(getMetrics(hpa)[0]).toEqual("unknown / 5k"); + }); + + it("should return correct object metrics with current value", () => { + const hpa = new HorizontalPodAutoscaler( + { + ...hpaV2, + spec: { + ...hpaV2.spec, + metrics: [ + { + type: HpaMetricType.Object, + object: { + metric: { + name: "requests-per-second", + }, + target: { + type: "Value", + value: "5k", + }, + }, + }, + ], + }, + status: { + currentReplicas: 1, + desiredReplicas: 10, + currentMetrics: [ + { + type: HpaMetricType.Object, + object: { + metric: { + name: "requests-per-second", + }, + current: { + value: "10k", + }, + }, + }, + ], + }, + }, + ); + + expect(getMetrics(hpa)[0]).toEqual("10k / 5k"); + }); + + it("should return correct external metrics with average value", () => { + const hpa = new HorizontalPodAutoscaler( + { + ...hpaV2, + spec: { + ...hpaV2.spec, + metrics: [ + { + type: HpaMetricType.External, + external: { + metric: { + name: "queue_messages_ready", + selector: { + matchLabels: { queue: "worker_tasks" }, + }, + }, + target: { + type: "AverageValue", + averageValue: "30", + }, + }, + }, + ], + }, + }, + ); + + expect(getMetrics(hpa)[0]).toEqual("unknown / 30 (avg)"); + }); + + it("should return correct external metrics with value", () => { + const hpa = new HorizontalPodAutoscaler( + { + ...hpaV2, + spec: { + ...hpaV2.spec, + metrics: [ + { + type: HpaMetricType.External, + external: { + metric: { + name: "queue_messages_ready", + selector: { + matchLabels: { queue: "worker_tasks" }, + }, + }, + target: { + type: "Value", + value: "30", + }, + }, + }, + ], + }, + }, + ); + + expect(getMetrics(hpa)[0]).toEqual("unknown / 30"); + }); + + it("should return correct external metrics with current value", () => { + const hpa = new HorizontalPodAutoscaler( + { + ...hpaV2, + spec: { + ...hpaV2.spec, + metrics: [ + { + type: HpaMetricType.External, + external: { + metric: { + name: "queue_messages_ready", + selector: { + matchLabels: { queue: "worker_tasks" }, + }, + }, + target: { + type: "Value", + value: "30", + }, + }, + }, + ], + }, + status: { + currentReplicas: 1, + desiredReplicas: 10, + currentMetrics: [ + { + type: HpaMetricType.External, + external: { + metric: { + name: "queue_messages_ready", + }, + current: { + value: "10", + }, + }, + }, + ], + }, + }, + ); + + expect(getMetrics(hpa)[0]).toEqual("10 / 30"); + }); + + it("should return correct external metrics with current average value", () => { + const hpa = new HorizontalPodAutoscaler( + { + ...hpaV2, + spec: { + ...hpaV2.spec, + metrics: [ + { + type: HpaMetricType.External, + external: { + metric: { + name: "queue_messages_ready", + selector: { + matchLabels: { queue: "worker_tasks" }, + }, + }, + target: { + type: "AverageValue", + averageValue: "30", + }, + }, + }, + ], + }, + status: { + currentReplicas: 1, + desiredReplicas: 10, + currentMetrics: [ + { + type: HpaMetricType.External, + external: { + metric: { + name: "queue_messages_ready", + }, + current: { + averageValue: "10", + }, + }, + }, + ], + }, + }, + ); + + expect(getMetrics(hpa)[0]).toEqual("10 (avg) / 30 (avg)"); + }); + + it("should return unknown current metrics if names are different", () => { + const hpa = new HorizontalPodAutoscaler( + { + ...hpaV2, + spec: { + ...hpaV2.spec, + metrics: [ + { + type: HpaMetricType.External, + external: { + metric: { + name: "queue_messages_ready", + selector: { + matchLabels: { queue: "worker_tasks" }, + }, + }, + target: { + type: "AverageValue", + averageValue: "30", + }, + }, + }, + ], + }, + status: { + currentReplicas: 1, + desiredReplicas: 10, + currentMetrics: [ + { + type: HpaMetricType.External, + external: { + metric: { + name: "queue_messages_NOT_ready", + }, + current: { + averageValue: "10", + }, + }, + }, + ], + }, + }, + ); + + expect(getMetrics(hpa)[0]).toEqual("unknown / 30 (avg)"); + }); + }); + + describe("HPA v2beta1", () => { + it("should return correct empty metrics", () => { + const hpa = new HorizontalPodAutoscaler(hpaV2Beta1); + + expect(getMetrics(hpa)).toHaveLength(0); + }); + + it("should return correct resource metrics", () => { + const hpa = new HorizontalPodAutoscaler({ + ...hpaV2Beta1, + spec: { + ...hpaV2Beta1.spec, + metrics: [ + { + type: HpaMetricType.Resource, + resource: { + name: "cpu", + targetAverageUtilization: 50, + }, + }, + ], + }, + }); + + expect(getMetrics(hpa)[0]).toEqual("unknown / 50%"); + }); + + it("should return correct resource metrics with current metrics", () => { + const hpa = new HorizontalPodAutoscaler({ + ...hpaV2Beta1, + spec: { + ...hpaV2Beta1.spec, + metrics: [ + { + type: HpaMetricType.Resource, + resource: { + name: "cpu", + targetAverageUtilization: 50, + }, + }, + ], + }, + status: { + currentReplicas: 1, + desiredReplicas: 10, + currentMetrics: [ + { + type: HpaMetricType.Resource, + resource: { + name: "cpu", + currentAverageUtilization: 10, + }, + }, + ], + }, + }); + + expect(getMetrics(hpa)[0]).toEqual("10% / 50%"); + }); + + it("should return correct resource metrics with current value", () => { + const hpa = new HorizontalPodAutoscaler({ + ...hpaV2Beta1, + spec: { + ...hpaV2Beta1.spec, + metrics: [ + { + type: HpaMetricType.Resource, + resource: { + name: "cpu", + targetAverageValue: "100m", + }, + }, + ], + }, + status: { + currentReplicas: 1, + desiredReplicas: 10, + currentMetrics: [ + { + type: HpaMetricType.Resource, + resource: { + name: "cpu", + currentAverageValue: "500m", + }, + }, + ], + }, + }); + + expect(getMetrics(hpa)[0]).toEqual("500m / 100m"); + }); + + it("should return correct container resource metrics", () => { + const hpa = new HorizontalPodAutoscaler( + { + ...hpaV2Beta1, + spec: { + ...hpaV2Beta1.spec, + metrics: [ + { + type: HpaMetricType.ContainerResource, + containerResource: { + name: "cpu", + container: "nginx", + targetAverageUtilization: 60, + }, + }, + ], + }, + }, + ); + + expect(getMetrics(hpa)[0]).toEqual("unknown / 60%"); + }); + + it("should return correct container resource metrics with current utilization value", () => { + const hpa = new HorizontalPodAutoscaler( + { + ...hpaV2Beta1, + spec: { + ...hpaV2Beta1.spec, + metrics: [ + { + type: HpaMetricType.ContainerResource, + containerResource: { + name: "cpu", + container: "nginx", + targetAverageUtilization: 60, + }, + }, + ], + }, + status: { + currentReplicas: 1, + desiredReplicas: 10, + currentMetrics: [ + { + type: HpaMetricType.ContainerResource, + containerResource: { + name: "cpu", + currentAverageUtilization: 10, + }, + }, + ], + }, + }, + ); + + expect(getMetrics(hpa)[0]).toEqual("10% / 60%"); + }); + + it("should return correct pod metrics", () => { + const hpa = new HorizontalPodAutoscaler( + { + ...hpaV2Beta1, + spec: { + ...hpaV2Beta1.spec, + metrics: [ + { + type: HpaMetricType.Pods, + pods: { + metricName: "packets-per-second", + targetAverageValue: "1k", + }, + }, + ], + }, + }, + ); + + expect(getMetrics(hpa)[0]).toEqual("unknown / 1k"); + }); + + it("should return correct pod metrics with current values", () => { + const hpa = new HorizontalPodAutoscaler( + { + ...hpaV2Beta1, + spec: { + ...hpaV2Beta1.spec, + metrics: [ + { + type: HpaMetricType.Pods, + pods: { + metricName: "packets-per-second", + + targetAverageValue: "1k", + }, + }, + ], + }, + status: { + currentReplicas: 1, + desiredReplicas: 10, + currentMetrics: [ + { + type: HpaMetricType.Pods, + pods: { + metricName: "packets-per-second", + currentAverageValue: "10", + }, + }, + ], + }, + }, + ); + + expect(getMetrics(hpa)[0]).toEqual("10 / 1k"); + }); + + it("should return correct object metrics", () => { + const hpa = new HorizontalPodAutoscaler( + { + ...hpaV2Beta1, + spec: { + ...hpaV2Beta1.spec, + metrics: [ + { + type: HpaMetricType.Object, + object: { + metricName: "packets-per-second", + targetValue: "10k", + }, + }, + ], + }, + }, + ); + + expect(getMetrics(hpa)[0]).toEqual("unknown / 10k"); + }); + + it("should return correct object metrics with average value", () => { + const hpa = new HorizontalPodAutoscaler( + { + ...hpaV2Beta1, + spec: { + ...hpaV2Beta1.spec, + metrics: [ + { + type: HpaMetricType.Object, + object: { + metricName: "packets-per-second", + averageValue: "5k", + }, + }, + ], + }, + }, + ); + + expect(getMetrics(hpa)[0]).toEqual("unknown / 5k"); + }); + + it("should return correct object metrics with current value", () => { + const hpa = new HorizontalPodAutoscaler( + { + ...hpaV2Beta1, + spec: { + ...hpaV2Beta1.spec, + metrics: [ + { + type: HpaMetricType.Object, + object: { + metricName: "packets-per-second", + targetValue: "5k", + }, + }, + ], + }, + status: { + currentReplicas: 1, + desiredReplicas: 10, + currentMetrics: [ + { + type: HpaMetricType.Object, + object: { + metricName: "packets-per-second", + currentValue: "10k", + }, + }, + ], + }, + }, + ); + + expect(getMetrics(hpa)[0]).toEqual("10k / 5k"); + }); + + it("should return correct external metrics with average value", () => { + const hpa = new HorizontalPodAutoscaler( + { + ...hpaV2Beta1, + spec: { + ...hpaV2Beta1.spec, + metrics: [ + { + type: HpaMetricType.External, + external: { + metricName: "queue_messages_ready", + metricSelector: { matchLabels: { queue: "worker_tasks" }}, + targetAverageValue: "30", + }, + }, + ], + }, + }, + ); + + expect(getMetrics(hpa)[0]).toEqual("unknown / 30"); + }); + + it("should return correct external metrics with value", () => { + const hpa = new HorizontalPodAutoscaler( + { + ...hpaV2Beta1, + spec: { + ...hpaV2Beta1.spec, + metrics: [ + { + type: HpaMetricType.External, + external: { + metricName: "queue_messages_ready", + metricSelector: { matchLabels: { queue: "worker_tasks" }}, + targetValue: "30", + }, + }, + ], + }, + }, + ); + + expect(getMetrics(hpa)[0]).toEqual("unknown / 30"); + }); + + it("should return correct external metrics with current value", () => { + const hpa = new HorizontalPodAutoscaler( + { + ...hpaV2Beta1, + spec: { + ...hpaV2Beta1.spec, + metrics: [ + { + type: HpaMetricType.External, + external: { + metricName: "queue_messages_ready", + metricSelector: { matchLabels: { queue: "worker_tasks" }}, + targetValue: "30", + }, + }, + ], + }, + status: { + currentReplicas: 1, + desiredReplicas: 10, + currentMetrics: [ + { + type: HpaMetricType.External, + external: { + metricName: "queue_messages_ready", + currentValue: "10", + }, + }, + ], + }, + }, + ); + + expect(getMetrics(hpa)[0]).toEqual("10 / 30"); + }); + + it("should return correct external metrics with current average value", () => { + const hpa = new HorizontalPodAutoscaler( + { + ...hpaV2Beta1, + spec: { + ...hpaV2Beta1.spec, + metrics: [ + { + type: HpaMetricType.External, + external: { + metricName: "queue_messages_ready", + metricSelector: { matchLabels: { queue: "worker_tasks" }}, + targetAverageValue: "30", + }, + }, + ], + }, + status: { + currentReplicas: 1, + desiredReplicas: 10, + currentMetrics: [ + { + type: HpaMetricType.External, + external: { + metricName: "queue_messages_ready", + currentAverageValue: "10", + }, + }, + ], + }, + }, + ); + + expect(getMetrics(hpa)[0]).toEqual("10 / 30"); + }); + + it("should return unknown current metrics if metric names are different", () => { + const hpa = new HorizontalPodAutoscaler({ + ...hpaV2Beta1, + spec: { + ...hpaV2Beta1.spec, + metrics: [ + { + type: HpaMetricType.Resource, + resource: { + name: "cpu", + targetAverageUtilization: 50, + }, + }, + ], + }, + status: { + currentReplicas: 1, + desiredReplicas: 10, + currentMetrics: [ + { + type: HpaMetricType.Resource, + resource: { + name: "memory", + currentAverageUtilization: 10, + }, + }, + ], + }, + }); + + expect(getMetrics(hpa)[0]).toEqual("unknown / 50%"); + }); + }); + + describe("HPA v1", () => { + it("should show target cpu utilization percentage", () => { + const hpa = new HorizontalPodAutoscaler({ + apiVersion: "autoscaling/v1", + kind: "HorizontalPodAutoscaler", + metadata: { + name: "hpav1", + resourceVersion: "1", + uid: "hpav1", + namespace: "default", + selfLink: "/apis/autoscaling/v1/namespaces/default/horizontalpodautoscalers/hpav1", + }, + spec: { + maxReplicas: 10, + scaleTargetRef: { + kind: "Deployment", + name: "hpav1deployment", + apiVersion: "apps/v1", + }, + targetCPUUtilizationPercentage: 80, + }, + }); + + expect(getMetrics(hpa)[0]).toEqual("unknown / 80%"); + }); + + it("should show current and target cpu utilization percentage", () => { + const hpa = new HorizontalPodAutoscaler({ + apiVersion: "autoscaling/v1", + kind: "HorizontalPodAutoscaler", + metadata: { + name: "hpav1", + resourceVersion: "1", + uid: "hpav1", + namespace: "default", + selfLink: "/apis/autoscaling/v1/namespaces/default/horizontalpodautoscalers/hpav1", + }, + spec: { + maxReplicas: 10, + scaleTargetRef: { + kind: "Deployment", + name: "hpav1deployment", + apiVersion: "apps/v1", + }, + targetCPUUtilizationPercentage: 80, + }, + status: { + currentReplicas: 1, + desiredReplicas: 10, + currentCPUUtilizationPercentage: 10, + }, + }); + + expect(getMetrics(hpa)[0]).toEqual("10% / 80%"); + }); + }); +}); diff --git a/src/renderer/components/+config-autoscalers/hpa-details.scss b/src/renderer/components/+config-autoscalers/hpa-details.scss index e10e14220f..571d988b11 100644 --- a/src/renderer/components/+config-autoscalers/hpa-details.scss +++ b/src/renderer/components/+config-autoscalers/hpa-details.scss @@ -11,9 +11,19 @@ } .metrics .Table { + margin: 0 (-$margin * 3); + .TableCell { word-break: break-word; + &:first-child { + margin-left: $margin * 2; + } + + &:last-child { + margin-right: $margin * 2; + } + &.name { flex-grow: 2; } diff --git a/src/renderer/components/+config-autoscalers/hpa-details.tsx b/src/renderer/components/+config-autoscalers/hpa-details.tsx index 4bd5f12a66..96c1a7e20b 100644 --- a/src/renderer/components/+config-autoscalers/hpa-details.tsx +++ b/src/renderer/components/+config-autoscalers/hpa-details.tsx @@ -22,6 +22,8 @@ import { withInjectables } from "@ogre-tools/injectable-react"; import apiManagerInjectable from "../../../common/k8s-api/api-manager/manager.injectable"; import getDetailsUrlInjectable from "../kube-detail-params/get-details-url.injectable"; import loggerInjectable from "../../../common/logger.injectable"; +import getHorizontalPodAutoscalerMetrics from "./get-hpa-metrics.injectable"; +import { getMetricName } from "./get-hpa-metric-name"; export interface HpaDetailsProps extends KubeObjectDetailsProps { } @@ -30,6 +32,7 @@ interface Dependencies { apiManager: ApiManager; logger: Logger; getDetailsUrl: GetDetailsUrl; + getMetrics: (hpa: HorizontalPodAutoscaler) => string[]; } @observer @@ -57,47 +60,46 @@ class NonInjectedHpaDetails extends React.Component { + const metricName = getMetricName(metric); + switch (metric.type) { case HpaMetricType.ContainerResource: // fallthrough case HpaMetricType.Resource: { const metricSpec = metric.resource ?? metric.containerResource; - const addition = metricSpec.targetAverageUtilization - ? " (as a percentage of request)" - : ""; - return `Resource ${metricSpec.name} on Pods${addition}`; + return `Resource ${metricSpec.name} on Pods`; } case HpaMetricType.Pods: - return `${metric.pods.metricName} on Pods`; + return `${metricName} on Pods`; case HpaMetricType.Object: { return ( <> - {metric.object.metricName} + {metricName} {" "} - {this.renderTargetLink(metric.object.target)} + {this.renderTargetLink(metric.object?.describedObject)} ); } case HpaMetricType.External: - return `${metric.external.metricName} on ${JSON.stringify(metric.external.metricSelector)}`; + return `${metricName} on ${JSON.stringify(metric.external.metricSelector ?? metric.external.metric?.selector)}`; } }; return ( - + Name Current / Target { - hpa.getMetrics() - .map((metric, index) => ( + this.props.getMetrics(hpa) + .map((metrics, index) => ( - {renderName(metric)} - {hpa.getMetricValues(metric)} + {renderName(hpa.getMetrics()[index])} + {metrics} )) } @@ -175,5 +177,6 @@ export const HpaDetails = withInjectables(NonInje apiManager: di.inject(apiManagerInjectable), getDetailsUrl: di.inject(getDetailsUrlInjectable), logger: di.inject(loggerInjectable), + getMetrics: di.inject(getHorizontalPodAutoscalerMetrics), }), }); diff --git a/src/renderer/components/+config-autoscalers/hpa-v1-metric-parser.ts b/src/renderer/components/+config-autoscalers/hpa-v1-metric-parser.ts new file mode 100644 index 0000000000..d5eee6b261 --- /dev/null +++ b/src/renderer/components/+config-autoscalers/hpa-v1-metric-parser.ts @@ -0,0 +1,70 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import type { MetricCurrentTarget, V2Beta1ContainerResourceMetricSource, V2Beta1ContainerResourceMetricStatus, V2Beta1ExternalMetricSource, V2Beta1ExternalMetricStatus, V2Beta1ObjectMetricSource, V2Beta1ObjectMetricStatus, V2Beta1PodsMetricSource, V2Beta1PodsMetricStatus, V2Beta1ResourceMetricSource, V2Beta1ResourceMetricStatus } from "../../../common/k8s-api/endpoints"; + +export class HorizontalPodAutoscalerV1MetricParser { + public getResource({ current, target }: { current: V2Beta1ResourceMetricStatus | undefined; target: V2Beta1ResourceMetricSource }): MetricCurrentTarget { + return { + current: ( + typeof current?.currentAverageUtilization === "number" + ? `${current.currentAverageUtilization}%` + : current?.currentAverageValue + ), + target: ( + typeof target?.targetAverageUtilization === "number" + ? `${target.targetAverageUtilization}%` + : target?.targetAverageValue + ), + }; + } + + public getPods({ current, target }: { current: V2Beta1PodsMetricStatus | undefined; target: V2Beta1PodsMetricSource }): MetricCurrentTarget { + return { + current: current?.currentAverageValue, + target: target?.targetAverageValue, + }; + } + + public getObject({ current, target }: { current: V2Beta1ObjectMetricStatus | undefined; target: V2Beta1ObjectMetricSource }): MetricCurrentTarget { + return { + current: ( + current?.currentValue + ?? current?.averageValue + ), + target: ( + target?.targetValue + ?? target?.averageValue + ), + }; + } + + public getExternal({ current, target }: { current: V2Beta1ExternalMetricStatus | undefined; target: V2Beta1ExternalMetricSource }): MetricCurrentTarget { + return { + current: ( + current?.currentValue + ?? current?.currentAverageValue + ), + target: ( + target?.targetValue + ?? target?.targetAverageValue + ), + }; + } + + public getContainerResource({ current, target }: { current: V2Beta1ContainerResourceMetricStatus | undefined; target: V2Beta1ContainerResourceMetricSource }): MetricCurrentTarget { + return { + current: ( + typeof current?.currentAverageUtilization === "number" + ? `${current.currentAverageUtilization}%` + : current?.currentAverageValue + ), + target: ( + typeof target?.targetAverageUtilization === "number" + ? `${target.targetAverageUtilization}%` + : target?.targetAverageValue + ), + }; + } +} diff --git a/src/renderer/components/+config-autoscalers/hpa-v2-metric-parser.ts b/src/renderer/components/+config-autoscalers/hpa-v2-metric-parser.ts new file mode 100644 index 0000000000..9b47b13780 --- /dev/null +++ b/src/renderer/components/+config-autoscalers/hpa-v2-metric-parser.ts @@ -0,0 +1,69 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import type { MetricCurrentTarget, V2ContainerResourceMetricSource, V2ContainerResourceMetricStatus, V2ExternalMetricSource, V2ExternalMetricStatus, V2ObjectMetricSource, V2ObjectMetricStatus, V2PodsMetricSource, V2PodsMetricStatus, V2ResourceMetricSource, V2ResourceMetricStatus } from "../../../common/k8s-api/endpoints"; + +export class HorizontalPodAutoscalerV2MetricParser { + public getResource({ current, target }: { current: V2ResourceMetricStatus | undefined; target: V2ResourceMetricSource }): MetricCurrentTarget { + return { + current: ( + typeof current?.current?.averageUtilization === "number" + ? `${current.current?.averageUtilization}%` + : current?.current?.averageValue + ), + target: typeof target?.target?.averageUtilization === "number" + ? `${target.target.averageUtilization}%` + : target?.target?.averageValue, + }; + } + + public getPods({ current, target }: { current: V2PodsMetricStatus | undefined; target: V2PodsMetricSource }): MetricCurrentTarget { + return { + current: current?.current?.averageValue, + target: target?.target?.averageValue, + }; + } + + public getObject({ current, target }: { current: V2ObjectMetricStatus | undefined; target: V2ObjectMetricSource }): MetricCurrentTarget { + return { + current: ( + current?.current?.value + ?? current?.current?.averageValue + ), + target: ( + target?.target?.value + ?? target?.target?.averageValue + ), + }; + } + + public getExternal({ current, target }: { current: V2ExternalMetricStatus | undefined; target: V2ExternalMetricSource }): MetricCurrentTarget { + const currentAverage = current?.current?.averageValue ? `${current?.current?.averageValue} (avg)` : undefined; + const targetAverage = target?.target?.averageValue ? `${target?.target?.averageValue} (avg)` : undefined; + + return { + current: ( + current?.current?.value + ?? currentAverage + ), + target: ( + target?.target?.value + ?? targetAverage + ), + }; + } + + public getContainerResource({ current, target }: { current: V2ContainerResourceMetricStatus | undefined; target: V2ContainerResourceMetricSource }): MetricCurrentTarget { + return { + current: ( + current?.current?.averageValue + ?? current?.current?.averageUtilization ? `${current?.current?.averageUtilization}%` : undefined + ), + target: ( + target?.target?.averageValue + ?? target?.target?.averageUtilization ? `${target?.target?.averageUtilization}%` : undefined + ), + }; + } +} diff --git a/src/renderer/components/+config-autoscalers/hpa.tsx b/src/renderer/components/+config-autoscalers/hpa.tsx index efdc7fd48d..e7a456b3f1 100644 --- a/src/renderer/components/+config-autoscalers/hpa.tsx +++ b/src/renderer/components/+config-autoscalers/hpa.tsx @@ -17,6 +17,7 @@ import { KubeObjectAge } from "../kube-object/age"; import type { HorizontalPodAutoscalerStore } from "./store"; import { withInjectables } from "@ogre-tools/injectable-react"; import horizontalPodAutoscalerStoreInjectable from "./store.injectable"; +import getHorizontalPodAutoscalerMetrics from "./get-hpa-metrics.injectable"; import { NamespaceSelectBadge } from "../+namespaces/namespace-select-badge"; enum columnId { @@ -32,6 +33,7 @@ enum columnId { interface Dependencies { horizontalPodAutoscalerStore: HorizontalPodAutoscalerStore; + getMetrics: (hpa: HorizontalPodAutoscaler) => string[]; } @observer @@ -39,7 +41,7 @@ class NonInjectedHorizontalPodAutoscalers extends React.Component getTargets(hpa: HorizontalPodAutoscaler) { const metrics = hpa.getMetrics(); - if (metrics.length === 0) { + if (metrics.length === 0 && !hpa.spec?.targetCPUUtilizationPercentage) { return

--

; } @@ -47,7 +49,7 @@ class NonInjectedHorizontalPodAutoscalers extends React.Component return (

- {hpa.getMetricValues(metrics[0])} + {this.props.getMetrics(hpa)[0]} {" "} {metricsRemain}

@@ -120,5 +122,6 @@ export const HorizontalPodAutoscalers = withInjectables(NonInjecte getProps: (di, props) => ({ ...props, horizontalPodAutoscalerStore: di.inject(horizontalPodAutoscalerStoreInjectable), + getMetrics: di.inject(getHorizontalPodAutoscalerMetrics), }), }); diff --git a/src/renderer/components/kube-object-details/kube-object-detail-items/implementations/horizontal-pod-autoscaler-detail-item.injectable.ts b/src/renderer/components/kube-object-details/kube-object-detail-items/implementations/horizontal-pod-autoscaler-detail-item.injectable.ts index 66dbebe7c5..dbf04d52d6 100644 --- a/src/renderer/components/kube-object-details/kube-object-detail-items/implementations/horizontal-pod-autoscaler-detail-item.injectable.ts +++ b/src/renderer/components/kube-object-details/kube-object-detail-items/implementations/horizontal-pod-autoscaler-detail-item.injectable.ts @@ -27,7 +27,7 @@ const horizontalPodAutoscalerDetailItemInjectable = getInjectable({ export const isHorizontalPodAutoscaler = kubeObjectMatchesToKindAndApiVersion( "HorizontalPodAutoscaler", - ["autoscaling/v2beta1"], + ["autoscaling/v2", "autoscaling/v2beta2", "autoscaling/v2beta1", "autoscaling/v1"], ); export default horizontalPodAutoscalerDetailItemInjectable;