From 38ca67c6a43945fe43f5a8ad6848f3d959262b3a Mon Sep 17 00:00:00 2001 From: Jim Ehrismann Date: Tue, 20 Dec 2022 17:47:05 -0500 Subject: [PATCH] introducing vpa resources (as hpa resources to start) vpa spec (WIP) adding real vpa fields scss files, and updates after rebase on master scss update tweaks to vpa api definition putting more fields into the vpa list adding vpa details, fixing vpa spec implementation (WIP) vpa details page done merge conflicts Signed-off-by: Jim Ehrismann --- .../vertical-pod-autoscaler.api.injectable.ts | 27 +++ .../endpoints/vertical-pod-autoscaler.api.ts | 139 +++++++++++ packages/core/src/common/rbac.ts | 7 +- .../src/extensions/renderer-api/k8s-api.ts | 5 +- ...l-pod-autoscaler-detail-item.injectable.ts | 2 +- ...l-pod-autoscaler-detail-item.injectable.ts | 33 +++ packages/core/src/renderer/utils/rbac.ts | 1 + .../create-resource/ClusterRole.yaml | 9 + ...-to-vertical-pod-autoscalers.injectable.ts | 20 ++ ...rtical-pod-autoscalers-route.injectable.ts | 24 ++ .../autoscaler.mixins.scss | 24 ++ .../+config-vertical-pod-autoscalers/index.ts | 7 + .../store.injectable.ts | 29 +++ .../+config-vertical-pod-autoscalers/store.ts | 10 + ...-auto-scalers-sidebar-items.injectable.tsx | 38 +++ ...-autoscalers-route-component.injectable.ts | 21 ++ .../vpa-details.scss | 22 ++ .../vpa-details.tsx | 220 ++++++++++++++++++ .../+config-vertical-pod-autoscalers/vpa.scss | 32 +++ .../+config-vertical-pod-autoscalers/vpa.tsx | 100 ++++++++ 20 files changed, 767 insertions(+), 3 deletions(-) create mode 100644 packages/core/src/common/k8s-api/endpoints/vertical-pod-autoscaler.api.injectable.ts create mode 100644 packages/core/src/common/k8s-api/endpoints/vertical-pod-autoscaler.api.ts create mode 100644 packages/core/src/renderer/components/kube-object-details/kube-object-detail-items/implementations/vertical-pod-autoscaler-detail-item.injectable.ts create mode 100644 src/common/front-end-routing/routes/cluster/config/vertical-pod-autoscalers/navigate-to-vertical-pod-autoscalers.injectable.ts create mode 100644 src/common/front-end-routing/routes/cluster/config/vertical-pod-autoscalers/vertical-pod-autoscalers-route.injectable.ts create mode 100644 src/renderer/components/+config-vertical-pod-autoscalers/autoscaler.mixins.scss create mode 100644 src/renderer/components/+config-vertical-pod-autoscalers/index.ts create mode 100644 src/renderer/components/+config-vertical-pod-autoscalers/store.injectable.ts create mode 100644 src/renderer/components/+config-vertical-pod-autoscalers/store.ts create mode 100644 src/renderer/components/+config-vertical-pod-autoscalers/vertical-pod-auto-scalers-sidebar-items.injectable.tsx create mode 100644 src/renderer/components/+config-vertical-pod-autoscalers/vertical-pod-autoscalers-route-component.injectable.ts create mode 100644 src/renderer/components/+config-vertical-pod-autoscalers/vpa-details.scss create mode 100644 src/renderer/components/+config-vertical-pod-autoscalers/vpa-details.tsx create mode 100644 src/renderer/components/+config-vertical-pod-autoscalers/vpa.scss create mode 100644 src/renderer/components/+config-vertical-pod-autoscalers/vpa.tsx diff --git a/packages/core/src/common/k8s-api/endpoints/vertical-pod-autoscaler.api.injectable.ts b/packages/core/src/common/k8s-api/endpoints/vertical-pod-autoscaler.api.injectable.ts new file mode 100644 index 0000000000..84dcb1899a --- /dev/null +++ b/packages/core/src/common/k8s-api/endpoints/vertical-pod-autoscaler.api.injectable.ts @@ -0,0 +1,27 @@ +/** + * 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 assert from "assert"; +import { storesAndApisCanBeCreatedInjectionToken } from "../stores-apis-can-be-created.token"; +import { VerticalPodAutoscalerApi } from "./vertical-pod-autoscaler.api"; +import { kubeApiInjectionToken } from "../kube-api/kube-api-injection-token"; +import loggerInjectable from "../../logger.injectable"; +import maybeKubeApiInjectable from "../maybe-kube-api.injectable"; + +const verticalPodAutoscalerApiInjectable = getInjectable({ + id: "vertical-pod-autoscaler-api", + instantiate: (di) => { + assert(di.inject(storesAndApisCanBeCreatedInjectionToken), "verticalPodAutoscalerApi is only available in certain environments"); + + return new VerticalPodAutoscalerApi({ + logger: di.inject(loggerInjectable), + maybeKubeApi: di.inject(maybeKubeApiInjectable), + }); + }, + + injectionToken: kubeApiInjectionToken, +}); + +export default verticalPodAutoscalerApiInjectable; diff --git a/packages/core/src/common/k8s-api/endpoints/vertical-pod-autoscaler.api.ts b/packages/core/src/common/k8s-api/endpoints/vertical-pod-autoscaler.api.ts new file mode 100644 index 0000000000..2db71b544b --- /dev/null +++ b/packages/core/src/common/k8s-api/endpoints/vertical-pod-autoscaler.api.ts @@ -0,0 +1,139 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import type { BaseKubeObjectCondition, NamespaceScopedMetadata } from "../kube-object"; +import { KubeObject } from "../kube-object"; +import type { DerivedKubeApiOptions, KubeApiDependencies } from "../kube-api"; +import { KubeApi } from "../kube-api"; + +export interface CrossVersionObjectReference { + kind: string; + name: string; + apiVersion?: string; +} + +export enum ResourceName { + ResourceCPU = "cpu", + ResourceMemory = "memory", + ResourceStorage = "storage", +} + +export type ResourceList = Partial>; + +export interface RecommendedContainerResources { + containerName?: string; + target: ResourceList; + lowerBound?: ResourceList; + upperBound?: ResourceList; + uncappedTarget?: ResourceList; +} +export interface RecommendedPodResources { + containerRecommendations?: RecommendedContainerResources[]; +} + +export interface VerticalPodAutoscalerStatus { + conditions?: BaseKubeObjectCondition[]; + recommendation?: RecommendedPodResources; +} + +export interface VerticalPodAutoscalerRecommenderSelector { + name: string; +} + +export enum ContainerScalingMode { + ContainerScalingModeAuto = "Auto", + ContainerScalingModeOff = "Off", +} + +export enum ControlledValues { + ControlledValueRequestsAndLimits = "RequestsAndLimits", + ControlledValueRequestsOnly = "RequestsOnly", +} + +// ContainerResourcePolicy controls how autoscaler computes the recommended resources for a specific container. +export interface ContainerResourcePolicy { + containerName?: string; + mode?: ContainerScalingMode; + minAllowed?: ResourceList; + maxAllowed?: ResourceList; + controlledResources?: ResourceName[]; + controlledValues?: ControlledValues; +} + +// Controls how the autoscaler computes recommended resources. +// The resource policy may be used to set constraints on the recommendations for individual containers. +// If not specified, the autoscaler computes recommended resources for all containers in the pod, without additional constraints. +export interface PodResourcePolicy { + containerPolicies?: ContainerResourcePolicy[]; // Per-container resource policies. +} + +export enum UpdateMode { + // UpdateModeOff means that autoscaler never changes Pod resources. + // The recommender still sets the recommended resources in the + // VerticalPodAutoscaler object. This can be used for a "dry run". + UpdateModeOff = "Off", + // UpdateModeInitial means that autoscaler only assigns resources on pod + // creation and does not change them during the lifetime of the pod. + UpdateModeInitial = "Initial", + // UpdateModeRecreate means that autoscaler assigns resources on pod + // creation and additionally can update them during the lifetime of the + // pod by deleting and recreating the pod. + UpdateModeRecreate = "Recreate", + // UpdateModeAuto means that autoscaler assigns resources on pod creation + // and additionally can update them during the lifetime of the pod, + // using any available update method. Currently this is equivalent to + // Recreate, which is the only available update method. + UpdateModeAuto = "Auto", +} +export interface PodUpdatePolicy { + minReplicas?: number; + updateMode?: UpdateMode; +} + +export interface VerticalPodAutoscalerSpec { + targetRef: CrossVersionObjectReference; + updatePolicy?: PodUpdatePolicy; + resourcePolicy?: PodResourcePolicy; + recommenders?: VerticalPodAutoscalerRecommenderSelector[]; +} + +export class VerticalPodAutoscaler extends KubeObject< + NamespaceScopedMetadata, + VerticalPodAutoscalerStatus, + VerticalPodAutoscalerSpec +> { + static readonly kind = "VerticalPodAutoscaler"; + static readonly namespaced = true; + static readonly apiBase = "/apis/autoscaling.k8s.io/v1/verticalpodautoscalers"; + + getReadyConditions() { + return this.getConditions().filter(({ isReady }) => isReady); + } + + getConditions() { + return this.status?.conditions?.map(condition => { + const { message, reason, lastTransitionTime, status } = condition; + + return { + ...condition, + isReady: status === "True", + tooltip: `${message || reason || ""} (${lastTransitionTime})`, + }; + }) ?? []; + } + + getMode() { + return this.spec.updatePolicy?.updateMode ?? UpdateMode.UpdateModeAuto; + } +} + +export class VerticalPodAutoscalerApi extends KubeApi { + constructor(deps: KubeApiDependencies, opts?: DerivedKubeApiOptions) { + super(deps, { + ...opts ?? {}, + objectConstructor: VerticalPodAutoscaler, + }); + } +} diff --git a/packages/core/src/common/rbac.ts b/packages/core/src/common/rbac.ts index 40b9f2c43c..e2ccad3806 100644 --- a/packages/core/src/common/rbac.ts +++ b/packages/core/src/common/rbac.ts @@ -7,7 +7,7 @@ export type KubeResource = "namespaces" | "nodes" | "events" | "resourcequotas" | "services" | "limitranges" | "leases" | "secrets" | "configmaps" | "ingresses" | "ingressclasses" | "networkpolicies" | "persistentvolumeclaims" | "persistentvolumes" | "storageclasses" | "pods" | "daemonsets" | "deployments" | "statefulsets" | "replicasets" | "jobs" | "cronjobs" | - "endpoints" | "customresourcedefinitions" | "horizontalpodautoscalers" | "podsecuritypolicies" | "poddisruptionbudgets" | + "endpoints" | "customresourcedefinitions" | "horizontalpodautoscalers" | "verticalpodautoscalers" | "podsecuritypolicies" | "poddisruptionbudgets" | "priorityclasses" | "runtimeclasses" | "roles" | "clusterroles" | "rolebindings" | "clusterrolebindings" | "serviceaccounts"; @@ -206,4 +206,9 @@ export const apiResourceRecord: Record = { group: "storage.k8s.io", namespaced: false, }, + verticalpodautoscalers: { + kind: "VerticalPodAutoscaler", + group: "autoscaling.k8s.io", + namespaced: true, + }, }; diff --git a/packages/core/src/extensions/renderer-api/k8s-api.ts b/packages/core/src/extensions/renderer-api/k8s-api.ts index 42b165602b..9fdec7fc57 100644 --- a/packages/core/src/extensions/renderer-api/k8s-api.ts +++ b/packages/core/src/extensions/renderer-api/k8s-api.ts @@ -23,6 +23,7 @@ import secretApiInjectable from "../../common/k8s-api/endpoints/secret.api.injec import resourceQuotaApiInjectable from "../../common/k8s-api/endpoints/resource-quota.api.injectable"; import limitRangeApiInjectable from "../../common/k8s-api/endpoints/limit-range.api.injectable"; import horizontalPodAutoscalerApiInjectable from "../../common/k8s-api/endpoints/horizontal-pod-autoscaler.api.injectable"; +import verticalPodAutoscalerApiInjectable from "../../common/k8s-api/endpoints/vertical-pod-autoscaler.api.injectable"; import podDisruptionBudgetApiInjectable from "../../common/k8s-api/endpoints/pod-disruption-budget.api.injectable"; import priorityClassStoreApiInjectable from "../../common/k8s-api/endpoints/priority-class.api.injectable"; import serviceApiInjectable from "../../common/k8s-api/endpoints/service.api.injectable"; @@ -76,6 +77,7 @@ export const resourceQuotaApi = asLegacyGlobalForExtensionApi(resourceQuotaApiIn export const limitRangeApi = asLegacyGlobalForExtensionApi(limitRangeApiInjectable); export const serviceApi = asLegacyGlobalForExtensionApi(serviceApiInjectable); export const hpaApi = asLegacyGlobalForExtensionApi(horizontalPodAutoscalerApiInjectable); +export const vpaApi = asLegacyGlobalForExtensionApi(verticalPodAutoscalerApiInjectable); export const pdbApi = asLegacyGlobalForExtensionApi(podDisruptionBudgetApiInjectable); export const pcApi = asLegacyGlobalForExtensionApi(priorityClassStoreApiInjectable); export const endpointApi = asLegacyGlobalForExtensionApi(endpointsApiInjectable); @@ -110,7 +112,8 @@ export type { SecretStore as SecretsStore } from "../../renderer/components/+con export type { ReplicaSetStore } from "../../renderer/components/+workloads-replicasets/store"; export type { ResourceQuotaStore as ResourceQuotasStore } from "../../renderer/components/+config-resource-quotas/store"; export type { LimitRangeStore as LimitRangesStore } from "../../renderer/components/+config-limit-ranges/store"; -export type { HorizontalPodAutoscalerStore as HPAStore } from "../../renderer/components/+config-autoscalers/store"; +export type { HorizontalPodAutoscalerStore as HPAStore } from "../../renderer/components/+config-horizontal-pod-autoscalers/store"; +export type { VerticalPodAutoscalerStore as VPAStore } from "../../renderer/components/+config-vertical-pod-autoscalers/store"; export type { PodDisruptionBudgetStore as PodDisruptionBudgetsStore } from "../../renderer/components/+config-pod-disruption-budgets/store"; export type { PriorityClassStore as PriorityClassStoreStore } from "../../renderer/components/+config-priority-classes/store"; export type { ServiceStore } from "../../renderer/components/+network-services/store"; diff --git a/packages/core/src/renderer/components/kube-object-details/kube-object-detail-items/implementations/horizontal-pod-autoscaler-detail-item.injectable.ts b/packages/core/src/renderer/components/kube-object-details/kube-object-detail-items/implementations/horizontal-pod-autoscaler-detail-item.injectable.ts index dbf04d52d6..8fe4b7da38 100644 --- a/packages/core/src/renderer/components/kube-object-details/kube-object-detail-items/implementations/horizontal-pod-autoscaler-detail-item.injectable.ts +++ b/packages/core/src/renderer/components/kube-object-details/kube-object-detail-items/implementations/horizontal-pod-autoscaler-detail-item.injectable.ts @@ -4,7 +4,7 @@ */ import { getInjectable } from "@ogre-tools/injectable"; import { kubeObjectDetailItemInjectionToken } from "../kube-object-detail-item-injection-token"; -import { HpaDetails } from "../../../+config-autoscalers"; +import { HpaDetails } from "../../../+config-horizontal-pod-autoscalers"; import { computed } from "mobx"; import { kubeObjectMatchesToKindAndApiVersion } from "../kube-object-matches-to-kind-and-api-version"; import currentKubeObjectInDetailsInjectable from "../../current-kube-object-in-details.injectable"; diff --git a/packages/core/src/renderer/components/kube-object-details/kube-object-detail-items/implementations/vertical-pod-autoscaler-detail-item.injectable.ts b/packages/core/src/renderer/components/kube-object-details/kube-object-detail-items/implementations/vertical-pod-autoscaler-detail-item.injectable.ts new file mode 100644 index 0000000000..3612fa2179 --- /dev/null +++ b/packages/core/src/renderer/components/kube-object-details/kube-object-detail-items/implementations/vertical-pod-autoscaler-detail-item.injectable.ts @@ -0,0 +1,33 @@ +/** + * 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 { kubeObjectDetailItemInjectionToken } from "../kube-object-detail-item-injection-token"; +import { VpaDetails } from "../../../+config-vertical-pod-autoscalers"; +import { computed } from "mobx"; +import { kubeObjectMatchesToKindAndApiVersion } from "../kube-object-matches-to-kind-and-api-version"; +import currentKubeObjectInDetailsInjectable from "../../current-kube-object-in-details.injectable"; + +const verticalPodAutoscalerDetailItemInjectable = getInjectable({ + id: "vertical-pod-autoscaler-detail-item", + + instantiate: (di) => { + const kubeObject = di.inject(currentKubeObjectInDetailsInjectable); + + return { + Component: VpaDetails, + enabled: computed(() => isVerticalPodAutoscaler(kubeObject.value.get()?.object)), + orderNumber: 10, + }; + }, + + injectionToken: kubeObjectDetailItemInjectionToken, +}); + +export const isVerticalPodAutoscaler = kubeObjectMatchesToKindAndApiVersion( + "VerticalPodAutoscaler", + ["autoscaling.k8s.io/v1"], +); + +export default verticalPodAutoscalerDetailItemInjectable; diff --git a/packages/core/src/renderer/utils/rbac.ts b/packages/core/src/renderer/utils/rbac.ts index 78fe66b273..00135a6492 100644 --- a/packages/core/src/renderer/utils/rbac.ts +++ b/packages/core/src/renderer/utils/rbac.ts @@ -42,6 +42,7 @@ export const ResourceNames: Record = { "clusterrolebindings": "Cluster Role Bindings", "clusterroles": "Cluster Roles", "serviceaccounts": "Service Accounts", + "verticalpodautoscalers": "Vertical Pod Autoscalers", }; export const ResourceKindMap = object.fromEntries( diff --git a/packages/open-lens/templates/create-resource/ClusterRole.yaml b/packages/open-lens/templates/create-resource/ClusterRole.yaml index fde73845ed..a3f48c522f 100644 --- a/packages/open-lens/templates/create-resource/ClusterRole.yaml +++ b/packages/open-lens/templates/create-resource/ClusterRole.yaml @@ -87,6 +87,15 @@ rules: - get - list - watch + - apiGroups: + - autoscaling.k8s.io + resources: + - verticalpodautoscalers + - verticalpodautoscalers/status + verbs: + - get + - list + - watch - apiGroups: - storage.k8s.io resources: diff --git a/src/common/front-end-routing/routes/cluster/config/vertical-pod-autoscalers/navigate-to-vertical-pod-autoscalers.injectable.ts b/src/common/front-end-routing/routes/cluster/config/vertical-pod-autoscalers/navigate-to-vertical-pod-autoscalers.injectable.ts new file mode 100644 index 0000000000..c453f03ddb --- /dev/null +++ b/src/common/front-end-routing/routes/cluster/config/vertical-pod-autoscalers/navigate-to-vertical-pod-autoscalers.injectable.ts @@ -0,0 +1,20 @@ +/** + * 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 verticalPodAutoscalersRouteInjectable from "./vertical-pod-autoscalers-route.injectable"; +import { navigateToRouteInjectionToken } from "../../../../navigate-to-route-injection-token"; + +const navigateToVerticalPodAutoscalersInjectable = getInjectable({ + id: "navigate-to-vertical-pod-autoscalers", + + instantiate: (di) => { + const navigateToRoute = di.inject(navigateToRouteInjectionToken); + const route = di.inject(verticalPodAutoscalersRouteInjectable); + + return () => navigateToRoute(route); + }, +}); + +export default navigateToVerticalPodAutoscalersInjectable; diff --git a/src/common/front-end-routing/routes/cluster/config/vertical-pod-autoscalers/vertical-pod-autoscalers-route.injectable.ts b/src/common/front-end-routing/routes/cluster/config/vertical-pod-autoscalers/vertical-pod-autoscalers-route.injectable.ts new file mode 100644 index 0000000000..57776c8237 --- /dev/null +++ b/src/common/front-end-routing/routes/cluster/config/vertical-pod-autoscalers/vertical-pod-autoscalers-route.injectable.ts @@ -0,0 +1,24 @@ +/** + * 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 { shouldShowResourceInjectionToken } from "../../../../../cluster-store/allowed-resources-injection-token"; +import { frontEndRouteInjectionToken } from "../../../../front-end-route-injection-token"; + +const verticalPodAutoscalersRouteInjectable = getInjectable({ + id: "vertical-pod-autoscalers-route", + + instantiate: (di) => ({ + path: "/vpa", + clusterFrame: true, + isEnabled: di.inject(shouldShowResourceInjectionToken, { + apiName: "verticalpodautoscalers", + group: "autoscaling.k8s.io", + }), + }), + + injectionToken: frontEndRouteInjectionToken, +}); + +export default verticalPodAutoscalersRouteInjectable; diff --git a/src/renderer/components/+config-vertical-pod-autoscalers/autoscaler.mixins.scss b/src/renderer/components/+config-vertical-pod-autoscalers/autoscaler.mixins.scss new file mode 100644 index 0000000000..f8804dcc93 --- /dev/null +++ b/src/renderer/components/+config-vertical-pod-autoscalers/autoscaler.mixins.scss @@ -0,0 +1,24 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +$vpa-status-colors: ( + recommendationprovided: var(--colorOk), + lowconfidence: var(--colorInfo), + fetchinghistory: var(--colorInfo), + nopodsmatched: var(--colorInfo), + configdeprecated: var(--colorSoftError), + configunsupported: var(--colorError), +); + +@mixin vpa-status-bgc { + + @each $status, + $color in $vpa-status-colors { + &.#{$status} { + background: $color; + color: white; + } + } +} \ No newline at end of file diff --git a/src/renderer/components/+config-vertical-pod-autoscalers/index.ts b/src/renderer/components/+config-vertical-pod-autoscalers/index.ts new file mode 100644 index 0000000000..64603e7f51 --- /dev/null +++ b/src/renderer/components/+config-vertical-pod-autoscalers/index.ts @@ -0,0 +1,7 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +export * from "./vpa"; +export * from "./vpa-details"; diff --git a/src/renderer/components/+config-vertical-pod-autoscalers/store.injectable.ts b/src/renderer/components/+config-vertical-pod-autoscalers/store.injectable.ts new file mode 100644 index 0000000000..583bba75f5 --- /dev/null +++ b/src/renderer/components/+config-vertical-pod-autoscalers/store.injectable.ts @@ -0,0 +1,29 @@ +/** + * 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 assert from "assert"; +import loggerInjectable from "../../../common/logger.injectable"; +import { kubeObjectStoreInjectionToken } from "../../../common/k8s-api/api-manager/kube-object-store-token"; +import verticalPodAutoscalerApiInjectable from "../../../common/k8s-api/endpoints/vertical-pod-autoscaler.api.injectable"; +import clusterFrameContextForNamespacedResourcesInjectable from "../../cluster-frame-context/for-namespaced-resources.injectable"; +import storesAndApisCanBeCreatedInjectable from "../../stores-apis-can-be-created.injectable"; +import { VerticalPodAutoscalerStore } from "./store"; + +const verticalPodAutoscalerStoreInjectable = getInjectable({ + id: "vertical-pod-autoscaler-store", + instantiate: (di) => { + assert(di.inject(storesAndApisCanBeCreatedInjectable), "verticalPodAutoscalerStore is only available in certain environments"); + + const api = di.inject(verticalPodAutoscalerApiInjectable); + + return new VerticalPodAutoscalerStore({ + context: di.inject(clusterFrameContextForNamespacedResourcesInjectable), + logger: di.inject(loggerInjectable), + }, api); + }, + injectionToken: kubeObjectStoreInjectionToken, +}); + +export default verticalPodAutoscalerStoreInjectable; diff --git a/src/renderer/components/+config-vertical-pod-autoscalers/store.ts b/src/renderer/components/+config-vertical-pod-autoscalers/store.ts new file mode 100644 index 0000000000..edb8efbdd0 --- /dev/null +++ b/src/renderer/components/+config-vertical-pod-autoscalers/store.ts @@ -0,0 +1,10 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { KubeObjectStore } from "../../../common/k8s-api/kube-object.store"; +import type { VerticalPodAutoscaler, VerticalPodAutoscalerApi } from "../../../common/k8s-api/endpoints/vertical-pod-autoscaler.api"; + +export class VerticalPodAutoscalerStore extends KubeObjectStore { +} diff --git a/src/renderer/components/+config-vertical-pod-autoscalers/vertical-pod-auto-scalers-sidebar-items.injectable.tsx b/src/renderer/components/+config-vertical-pod-autoscalers/vertical-pod-auto-scalers-sidebar-items.injectable.tsx new file mode 100644 index 0000000000..8319484361 --- /dev/null +++ b/src/renderer/components/+config-vertical-pod-autoscalers/vertical-pod-auto-scalers-sidebar-items.injectable.tsx @@ -0,0 +1,38 @@ +/** + * 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 { computed } from "mobx"; +import verticalPodAutoscalersRouteInjectable from "../../../common/front-end-routing/routes/cluster/config/vertical-pod-autoscalers/vertical-pod-autoscalers-route.injectable"; +import { configSidebarItemId } from "../+config/config-sidebar-items.injectable"; +import type { SidebarItemRegistration } from "../layout/sidebar-items.injectable"; +import { sidebarItemsInjectionToken } from "../layout/sidebar-items.injectable"; +import routeIsActiveInjectable from "../../routes/route-is-active.injectable"; +import navigateToVerticalPodAutoscalersInjectable from "../../../common/front-end-routing/routes/cluster/config/vertical-pod-autoscalers/navigate-to-vertical-pod-autoscalers.injectable"; + +const verticalPodAutoScalersSidebarItemsInjectable = getInjectable({ + id: "vertical-pod-auto-scalers-sidebar-items", + + instantiate: (di) => { + const route = di.inject(verticalPodAutoscalersRouteInjectable); + const navigateToVerticalPodAutoscalers = di.inject(navigateToVerticalPodAutoscalersInjectable); + const routeIsActive = di.inject(routeIsActiveInjectable, route); + + return computed((): SidebarItemRegistration[] => [ + { + id: "vertical-pod-auto-scalers", + parentId: configSidebarItemId, + title: "VPA", + onClick: navigateToVerticalPodAutoscalers, + isActive: routeIsActive, + isVisible: route.isEnabled, + orderNumber: 50, + }, + ]); + }, + + injectionToken: sidebarItemsInjectionToken, +}); + +export default verticalPodAutoScalersSidebarItemsInjectable; diff --git a/src/renderer/components/+config-vertical-pod-autoscalers/vertical-pod-autoscalers-route-component.injectable.ts b/src/renderer/components/+config-vertical-pod-autoscalers/vertical-pod-autoscalers-route-component.injectable.ts new file mode 100644 index 0000000000..999a77e3d4 --- /dev/null +++ b/src/renderer/components/+config-vertical-pod-autoscalers/vertical-pod-autoscalers-route-component.injectable.ts @@ -0,0 +1,21 @@ +/** + * 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 { routeSpecificComponentInjectionToken } from "../../routes/route-specific-component-injection-token"; +import verticalPodAutoscalersRouteInjectable from "../../../common/front-end-routing/routes/cluster/config/vertical-pod-autoscalers/vertical-pod-autoscalers-route.injectable"; +import { VerticalPodAutoscalers } from "./vpa"; + +const verticalPodAutoscalersRouteComponentInjectable = getInjectable({ + id: "vertical-pod-autoscalers-route-component", + + instantiate: (di) => ({ + route: di.inject(verticalPodAutoscalersRouteInjectable), + Component: VerticalPodAutoscalers, + }), + + injectionToken: routeSpecificComponentInjectionToken, +}); + +export default verticalPodAutoscalersRouteComponentInjectable; diff --git a/src/renderer/components/+config-vertical-pod-autoscalers/vpa-details.scss b/src/renderer/components/+config-vertical-pod-autoscalers/vpa-details.scss new file mode 100644 index 0000000000..c0f10f8a37 --- /dev/null +++ b/src/renderer/components/+config-vertical-pod-autoscalers/vpa-details.scss @@ -0,0 +1,22 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +@import "autoscaler.mixins"; + +.VpaDetails { + .status { + @include vpa-status-bgc; + } + + .metrics .Table { + .TableCell { + word-break: break-word; + + &.name { + flex-grow: 2; + } + } + } +} \ No newline at end of file diff --git a/src/renderer/components/+config-vertical-pod-autoscalers/vpa-details.tsx b/src/renderer/components/+config-vertical-pod-autoscalers/vpa-details.tsx new file mode 100644 index 0000000000..f008fe3350 --- /dev/null +++ b/src/renderer/components/+config-vertical-pod-autoscalers/vpa-details.tsx @@ -0,0 +1,220 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import "./vpa-details.scss"; + +import startCase from "lodash/startCase"; +import React from "react"; +import { observer } from "mobx-react"; +import { Link } from "react-router-dom"; +import { DrawerItem, DrawerTitle } from "../drawer"; +import { Badge } from "../badge"; +import type { KubeObjectDetailsProps } from "../kube-object-details"; +import { cssNames } from "../../utils"; +import { ContainerScalingMode, ControlledValues, ResourceName, UpdateMode, VerticalPodAutoscaler } from "../../../common/k8s-api/endpoints/vertical-pod-autoscaler.api"; +import type { PodUpdatePolicy, PodResourcePolicy, VerticalPodAutoscalerStatus } from "../../../common/k8s-api/endpoints/vertical-pod-autoscaler.api"; +import type { ApiManager } from "../../../common/k8s-api/api-manager"; +import loggerInjectable from "../../../common/logger.injectable"; +import type { Logger } from "../../../common/logger"; +import type { GetDetailsUrl } from "../kube-detail-params/get-details-url.injectable"; +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"; + +export interface VpaDetailsProps extends KubeObjectDetailsProps { +} + +interface Dependencies { + apiManager: ApiManager; + getDetailsUrl: GetDetailsUrl; + logger: Logger; +} + +@observer +class NonInjectedVpaDetails extends React.Component { + renderStatus(status: VerticalPodAutoscalerStatus) { + const { recommendation } = status; + const { object: vpa } = this.props; + + return ( +
+ Status + + {vpa.getReadyConditions() + .map(({ type, tooltip, isReady }) => ( + + ))} + + + {recommendation?.containerRecommendations && ( + recommendation.containerRecommendations + .map( ({ containerName, target, lowerBound, upperBound, uncappedTarget }) => { + return ( +
+ {`Container Recommendation for ${containerName}`} + + {Object.entries(target).map(([name, value]) => ( + + {value} + + ))} + + {lowerBound && ( + + {Object.entries(lowerBound).map(([name, value]) => ( + + {value} + + ))} + + )} + {upperBound && ( + + {Object.entries(upperBound).map(([name, value]) => ( + + {value} + + ))} + + )} + {uncappedTarget && ( + + {Object.entries(uncappedTarget).map(([name, value]) => ( + + {value} + + ))} + + )} +
+ ); + }) + )} +
+ ); + } + + renderUpdatePolicy(updatePolicy: PodUpdatePolicy) { + return ( +
+ Update Policy + + {updatePolicy?.updateMode ?? UpdateMode.UpdateModeAuto} + + + {updatePolicy?.minReplicas} + +
+ ); + } + + renderResourcePolicy(resourcePolicy: PodResourcePolicy) { + return ( +
+ {resourcePolicy.containerPolicies && ( +
+ {resourcePolicy.containerPolicies + .map( ({ containerName, mode, minAllowed, maxAllowed, controlledResources, controlledValues }) => { + return ( +
+ {`Container Policy for ${containerName}`} + + {mode ?? ContainerScalingMode.ContainerScalingModeAuto} + + {minAllowed && ( + + {Object.entries(minAllowed).map(([name, value]) => ( + + {value} + + ))} + + )} + {maxAllowed && ( + + {Object.entries(maxAllowed).map(([name, value]) => ( + + {value} + + ))} + + )} + + {controlledResources?.length ? controlledResources.join(", ") : `${ResourceName.ResourceCPU}, ${ResourceName.ResourceMemory}`} + + + {controlledValues ?? ControlledValues.ControlledValueRequestsAndLimits} + +
+ ); + }) + } +
+ )} +
+ ); + } + + render() { + const { object: vpa, apiManager, getDetailsUrl, logger } = this.props; + + if (!vpa) { + return null; + } + + if (!(vpa instanceof VerticalPodAutoscaler)) { + logger.error("[VpaDetails]: passed object that is not an instanceof VerticalPodAutoscaler", vpa); + + return null; + } + + const { targetRef, recommenders, resourcePolicy, updatePolicy } = vpa.spec; + + return ( +
+ + {targetRef && ( + + {targetRef.kind} + / + {targetRef.name} + + )} + + + + { + /* according to the spec there can be 0 or 1 recommenders, only */ + recommenders?.length ? recommenders[0].name : "default" + } + + + {vpa.status && this.renderStatus(vpa.status)} + {updatePolicy && this.renderUpdatePolicy(updatePolicy)} + {resourcePolicy && this.renderResourcePolicy(resourcePolicy)} + + CRD details +
+ ); + } +} + +export const VpaDetails = withInjectables(NonInjectedVpaDetails, { + getProps: (di, props) => ({ + ...props, + apiManager: di.inject(apiManagerInjectable), + getDetailsUrl: di.inject(getDetailsUrlInjectable), + logger: di.inject(loggerInjectable), + }), +}); diff --git a/src/renderer/components/+config-vertical-pod-autoscalers/vpa.scss b/src/renderer/components/+config-vertical-pod-autoscalers/vpa.scss new file mode 100644 index 0000000000..49bf5b4ca3 --- /dev/null +++ b/src/renderer/components/+config-vertical-pod-autoscalers/vpa.scss @@ -0,0 +1,32 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +@import "autoscaler.mixins"; + +.VerticalPodAutoscalers { + .TableCell { + &.name { + flex: 1.5; + } + + &.warning { + @include table-cell-warning; + } + + &.metrics { + flex: 1.5; + } + + &.status { + flex: 1.5; + @include table-cell-labels-offsets; + @include vpa-status-bgc; + } + + &.age { + flex: .5; + } + } +} \ No newline at end of file diff --git a/src/renderer/components/+config-vertical-pod-autoscalers/vpa.tsx b/src/renderer/components/+config-vertical-pod-autoscalers/vpa.tsx new file mode 100644 index 0000000000..f2657cbf50 --- /dev/null +++ b/src/renderer/components/+config-vertical-pod-autoscalers/vpa.tsx @@ -0,0 +1,100 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import "./vpa.scss"; + +import React from "react"; +import { observer } from "mobx-react"; +import { KubeObjectListLayout } from "../kube-object-list-layout"; +// import type { VerticalPodAutoscaler } from "../../../common/k8s-api/endpoints/vertical-pod-autoscaler.api"; +import { Badge } from "../badge"; +import { cssNames, prevDefault } from "../../utils"; +import { KubeObjectStatusIcon } from "../kube-object-status-icon"; +import { SiblingsInTabLayout } from "../layout/siblings-in-tab-layout"; +import { KubeObjectAge } from "../kube-object/age"; +import type { VerticalPodAutoscalerStore } from "./store"; +import type { FilterByNamespace } from "../+namespaces/namespace-select-filter-model/filter-by-namespace.injectable"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import filterByNamespaceInjectable from "../+namespaces/namespace-select-filter-model/filter-by-namespace.injectable"; +import verticalPodAutoscalerStoreInjectable from "./store.injectable"; + +enum columnId { + name = "name", + namespace = "namespace", + mode = "mode", + age = "age", + status = "status", +} + +interface Dependencies { + verticalPodAutoscalerStore: VerticalPodAutoscalerStore; + filterByNamespace: FilterByNamespace; +} + +@observer +class NonInjectedVerticalPodAutoscalers extends React.Component { + render() { + return ( + + vpa.getName(), + [columnId.namespace]: vpa => vpa.getNs(), + [columnId.age]: vpa => -vpa.getCreationTimestamp(), + }} + searchFilters={[ + vpa => vpa.getSearchFields(), + ]} + renderHeaderTitle="Vertical Pod Autoscalers" + renderTableHeader={[ + { title: "Name", className: "name", sortBy: columnId.name }, + { className: "warning", showWithColumn: columnId.name }, + { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace }, + { title: "Mode", className: "mode", sortBy: columnId.mode, id: columnId.mode }, + { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, + { title: "Status", className: "status scrollable", id: columnId.status }, + ]} + renderTableContents={vpa => [ + vpa.getName(), + , + this.props.filterByNamespace(vpa.getNs()))} + > + {vpa.getNs()} + , + vpa.getMode(), + , + vpa.getConditions() + .filter(({ isReady }) => isReady) + .map(({ type, tooltip }) => ( + + )), + ]} + /> + + ); + } +} + +export const VerticalPodAutoscalers = withInjectables(NonInjectedVerticalPodAutoscalers, { + getProps: (di, props) => ({ + ...props, + filterByNamespace: di.inject(filterByNamespaceInjectable), + verticalPodAutoscalerStore: di.inject(verticalPodAutoscalerStoreInjectable), + }), +});