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), + }), +});