diff --git a/integration/__tests__/app.tests.ts b/integration/__tests__/app.tests.ts index 410712e4f0..d68ab2f011 100644 --- a/integration/__tests__/app.tests.ts +++ b/integration/__tests__/app.tests.ts @@ -313,6 +313,12 @@ describe("Lens integration tests", () => { expectedSelector: "h5.title", expectedText: "Resource Quotas" }, + { + name: "Limit Ranges", + href: "limitranges", + expectedSelector: "h5.title", + expectedText: "Limit Ranges" + }, { name: "HPA", href: "hpa", diff --git a/src/common/rbac.ts b/src/common/rbac.ts index bd003e87a1..fbcf7c98d8 100644 --- a/src/common/rbac.ts +++ b/src/common/rbac.ts @@ -1,7 +1,7 @@ import { getHostedCluster } from "./cluster-store"; export type KubeResource = - "namespaces" | "nodes" | "events" | "resourcequotas" | "services" | + "namespaces" | "nodes" | "events" | "resourcequotas" | "services" | "limitranges" | "secrets" | "configmaps" | "ingresses" | "networkpolicies" | "persistentvolumeclaims" | "persistentvolumes" | "storageclasses" | "pods" | "daemonsets" | "deployments" | "statefulsets" | "replicasets" | "jobs" | "cronjobs" | "endpoints" | "customresourcedefinitions" | "horizontalpodautoscalers" | "podsecuritypolicies" | "poddisruptionbudgets"; @@ -23,6 +23,7 @@ export const apiResources: KubeApiResource[] = [ { resource: "horizontalpodautoscalers" }, { resource: "ingresses", group: "networking.k8s.io" }, { resource: "jobs", group: "batch" }, + { resource: "limitranges" }, { resource: "namespaces" }, { resource: "networkpolicies", group: "networking.k8s.io" }, { resource: "nodes" }, diff --git a/src/extensions/renderer-api/k8s-api.ts b/src/extensions/renderer-api/k8s-api.ts index fe04550fb7..071d8365ab 100644 --- a/src/extensions/renderer-api/k8s-api.ts +++ b/src/extensions/renderer-api/k8s-api.ts @@ -14,6 +14,7 @@ export { ConfigMap, configMapApi } from "../../renderer/api/endpoints"; export { Secret, secretsApi, ISecretRef } from "../../renderer/api/endpoints"; export { ReplicaSet, replicaSetApi } from "../../renderer/api/endpoints"; export { ResourceQuota, resourceQuotaApi } from "../../renderer/api/endpoints"; +export { LimitRange, limitRangeApi } from "../../renderer/api/endpoints"; export { HorizontalPodAutoscaler, hpaApi } from "../../renderer/api/endpoints"; export { PodDisruptionBudget, pdbApi } from "../../renderer/api/endpoints"; export { Service, serviceApi } from "../../renderer/api/endpoints"; @@ -46,6 +47,7 @@ export type { ConfigMapsStore } from "../../renderer/components/+config-maps/con export type { SecretsStore } from "../../renderer/components/+config-secrets/secrets.store"; export type { ReplicaSetStore } from "../../renderer/components/+workloads-replicasets/replicasets.store"; export type { ResourceQuotasStore } from "../../renderer/components/+config-resource-quotas/resource-quotas.store"; +export type { LimitRangesStore } from "../../renderer/components/+config-limit-ranges/limit-ranges.store"; export type { HPAStore } from "../../renderer/components/+config-autoscalers/hpa.store"; export type { PodDisruptionBudgetsStore } from "../../renderer/components/+config-pod-disruption-budgets/pod-disruption-budgets.store"; export type { ServiceStore } from "../../renderer/components/+network-services/services.store"; diff --git a/src/renderer/api/endpoints/index.ts b/src/renderer/api/endpoints/index.ts index f1202b9122..5ab54e7c3a 100644 --- a/src/renderer/api/endpoints/index.ts +++ b/src/renderer/api/endpoints/index.ts @@ -14,6 +14,7 @@ export * from "./events.api"; export * from "./hpa.api"; export * from "./ingress.api"; export * from "./job.api"; +export * from "./limit-range.api"; export * from "./namespaces.api"; export * from "./network-policy.api"; export * from "./nodes.api"; diff --git a/src/renderer/api/endpoints/limit-range.api.ts b/src/renderer/api/endpoints/limit-range.api.ts new file mode 100644 index 0000000000..bbb3941c87 --- /dev/null +++ b/src/renderer/api/endpoints/limit-range.api.ts @@ -0,0 +1,57 @@ +import { KubeObject } from "../kube-object"; +import { KubeApi } from "../kube-api"; +import { autobind } from "../../utils"; + +export enum LimitType { + CONTAINER = "Container", + POD = "Pod", + PVC = "PersistentVolumeClaim", +} + +export enum Resource { + MEMORY = "memory", + CPU = "cpu", + STORAGE = "storage", + EPHEMERAL_STORAGE = "ephemeral-storage", +} + +export enum LimitPart { + MAX = "max", + MIN = "min", + DEFAULT = "default", + DEFAULT_REQUEST = "defaultRequest", + MAX_LIMIT_REQUEST_RATIO = "maxLimitRequestRatio", +} + +type LimitRangeParts = Partial>>; + +export interface LimitRangeItem extends LimitRangeParts { + type: string +} + +@autobind() +export class LimitRange extends KubeObject { + static kind = "LimitRange"; + static namespaced = true; + static apiBase = "/api/v1/limitranges"; + + spec: { + limits: LimitRangeItem[]; + }; + + getContainerLimits() { + return this.spec.limits.filter(limit => limit.type === LimitType.CONTAINER); + } + + getPodLimits() { + return this.spec.limits.filter(limit => limit.type === LimitType.POD); + } + + getPVCLimits() { + return this.spec.limits.filter(limit => limit.type === LimitType.PVC); + } +} + +export const limitRangeApi = new KubeApi({ + objectConstructor: LimitRange, +}); diff --git a/src/renderer/components/+config-limit-ranges/index.ts b/src/renderer/components/+config-limit-ranges/index.ts new file mode 100644 index 0000000000..53308bdd18 --- /dev/null +++ b/src/renderer/components/+config-limit-ranges/index.ts @@ -0,0 +1,3 @@ +export * from "./limit-ranges"; +export * from "./limit-ranges.route"; +export * from "./limit-range-details"; diff --git a/src/renderer/components/+config-limit-ranges/limit-range-details.scss b/src/renderer/components/+config-limit-ranges/limit-range-details.scss new file mode 100644 index 0000000000..ff39ec514a --- /dev/null +++ b/src/renderer/components/+config-limit-ranges/limit-range-details.scss @@ -0,0 +1,12 @@ +.LimitRangeDetails { + + .DrawerItem { + > .name { + font-weight: $font-weight-normal; + padding-left: 4px; + } + .DrawerItem { + padding-top: 4px; + } + } +} diff --git a/src/renderer/components/+config-limit-ranges/limit-range-details.tsx b/src/renderer/components/+config-limit-ranges/limit-range-details.tsx new file mode 100644 index 0000000000..ab35505cc6 --- /dev/null +++ b/src/renderer/components/+config-limit-ranges/limit-range-details.tsx @@ -0,0 +1,97 @@ +import "./limit-range-details.scss"; + +import React from "react"; +import { observer } from "mobx-react"; +import { KubeObjectDetailsProps } from "../kube-object"; +import { LimitPart, LimitRange, LimitRangeItem, Resource } from "../../api/endpoints/limit-range.api"; +import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry"; +import { KubeObjectMeta } from "../kube-object/kube-object-meta"; +import { DrawerItem } from "../drawer/drawer-item"; +import { Badge } from "../badge"; + +interface Props extends KubeObjectDetailsProps { +} + +function renderLimit(limit: LimitRangeItem, part: LimitPart, resource: Resource) { + + const resourceLimit = limit[part]?.[resource]; + + if (!resourceLimit) { + return null; + } + + return ; +} + +function renderResourceLimits(limit: LimitRangeItem, resource: Resource) { + return ( + + {renderLimit(limit, LimitPart.MIN, resource)} + {renderLimit(limit, LimitPart.MAX, resource)} + {renderLimit(limit, LimitPart.DEFAULT, resource)} + {renderLimit(limit, LimitPart.DEFAULT_REQUEST, resource)} + {renderLimit(limit, LimitPart.MAX_LIMIT_REQUEST_RATIO, resource)} + + ); +} + +function renderLimitDetails(limits: LimitRangeItem[], resources: Resource[]) { + + return resources.map(resource => + + { + limits.map(limit => + renderResourceLimits(limit, resource) + ) + } + + ); +} + +@observer +export class LimitRangeDetails extends React.Component { + render() { + const { object: limitRange } = this.props; + + if (!limitRange) return null; + const containerLimits = limitRange.getContainerLimits(); + const podLimits = limitRange.getPodLimits(); + const pvcLimits = limitRange.getPVCLimits(); + + return ( +
+ + + {containerLimits.length > 0 && + + { + renderLimitDetails(containerLimits, [Resource.CPU, Resource.MEMORY, Resource.EPHEMERAL_STORAGE]) + } + + } + {podLimits.length > 0 && + + { + renderLimitDetails(podLimits, [Resource.CPU, Resource.MEMORY, Resource.EPHEMERAL_STORAGE]) + } + + } + {pvcLimits.length > 0 && + + { + renderLimitDetails(pvcLimits, [Resource.STORAGE]) + } + + } +
+ ); + } +} + +kubeObjectDetailRegistry.add({ + kind: "LimitRange", + apiVersions: ["v1"], + components: { + Details: (props) => + } +}); diff --git a/src/renderer/components/+config-limit-ranges/limit-ranges.route.ts b/src/renderer/components/+config-limit-ranges/limit-ranges.route.ts new file mode 100644 index 0000000000..09e3052350 --- /dev/null +++ b/src/renderer/components/+config-limit-ranges/limit-ranges.route.ts @@ -0,0 +1,11 @@ +import type { RouteProps } from "react-router"; +import { buildURL } from "../../../common/utils/buildUrl"; + +export const limitRangesRoute: RouteProps = { + path: "/limitranges" +}; + +export interface LimitRangeRouteParams { +} + +export const limitRangeURL = buildURL(limitRangesRoute.path); diff --git a/src/renderer/components/+config-limit-ranges/limit-ranges.scss b/src/renderer/components/+config-limit-ranges/limit-ranges.scss new file mode 100644 index 0000000000..e5de19acfb --- /dev/null +++ b/src/renderer/components/+config-limit-ranges/limit-ranges.scss @@ -0,0 +1,7 @@ +.LimitRanges { + .TableCell { + &.warning { + @include table-cell-warning; + } + } +} diff --git a/src/renderer/components/+config-limit-ranges/limit-ranges.store.ts b/src/renderer/components/+config-limit-ranges/limit-ranges.store.ts new file mode 100644 index 0000000000..bd760efadd --- /dev/null +++ b/src/renderer/components/+config-limit-ranges/limit-ranges.store.ts @@ -0,0 +1,12 @@ +import { autobind } from "../../../common/utils/autobind"; +import { KubeObjectStore } from "../../kube-object.store"; +import { apiManager } from "../../api/api-manager"; +import { LimitRange, limitRangeApi } from "../../api/endpoints/limit-range.api"; + +@autobind() +export class LimitRangesStore extends KubeObjectStore { + api = limitRangeApi; +} + +export const limitRangeStore = new LimitRangesStore(); +apiManager.registerStore(limitRangeStore); diff --git a/src/renderer/components/+config-limit-ranges/limit-ranges.tsx b/src/renderer/components/+config-limit-ranges/limit-ranges.tsx new file mode 100644 index 0000000000..8bb498c1c0 --- /dev/null +++ b/src/renderer/components/+config-limit-ranges/limit-ranges.tsx @@ -0,0 +1,53 @@ +import "./limit-ranges.scss"; + +import { RouteComponentProps } from "react-router"; +import { observer } from "mobx-react"; +import { KubeObjectListLayout } from "../kube-object/kube-object-list-layout"; +import { limitRangeStore } from "./limit-ranges.store"; +import { LimitRangeRouteParams } from "./limit-ranges.route"; +import React from "react"; +import { KubeObjectStatusIcon } from "../kube-object-status-icon"; +import { LimitRange } from "../../api/endpoints/limit-range.api"; + +enum sortBy { + name = "name", + namespace = "namespace", + age = "age", +} + +interface Props extends RouteComponentProps { +} + +@observer +export class LimitRanges extends React.Component { + render() { + return ( + item.getName(), + [sortBy.namespace]: (item: LimitRange) => item.getNs(), + [sortBy.age]: (item: LimitRange) => item.metadata.creationTimestamp, + }} + searchFilters={[ + (item: LimitRange) => item.getName(), + (item: LimitRange) => item.getNs(), + ]} + renderHeaderTitle={"Limit Ranges"} + renderTableHeader={[ + { title: "Name", className: "name", sortBy: sortBy.name }, + { className: "warning" }, + { title: "Namespace", className: "namespace", sortBy: sortBy.namespace }, + { title: "Age", className: "age", sortBy: sortBy.age }, + ]} + renderTableContents={(limitRange: LimitRange) => [ + limitRange.getName(), + , + limitRange.getNs(), + limitRange.getAge(), + ]} + /> + ); + } +} diff --git a/src/renderer/components/+config/config.tsx b/src/renderer/components/+config/config.tsx index bb70dd3fb5..e3158459ba 100644 --- a/src/renderer/components/+config/config.tsx +++ b/src/renderer/components/+config/config.tsx @@ -8,6 +8,7 @@ import { resourceQuotaRoute, ResourceQuotas, resourceQuotaURL } from "../+config import { pdbRoute, pdbURL, PodDisruptionBudgets } from "../+config-pod-disruption-budgets"; import { HorizontalPodAutoscalers, hpaRoute, hpaURL } from "../+config-autoscalers"; import { isAllowedResource } from "../../../common/rbac"; +import { LimitRanges, limitRangesRoute, limitRangeURL } from "../+config-limit-ranges"; @observer export class Config extends React.Component { @@ -42,6 +43,15 @@ export class Config extends React.Component { }); } + if (isAllowedResource("limitranges")) { + routes.push({ + title: "Limit Ranges", + component: LimitRanges, + url: limitRangeURL({ query }), + routePath: limitRangesRoute.path.toString(), + }); + } + if (isAllowedResource("horizontalpodautoscalers")) { routes.push({ title: "HPA", diff --git a/src/renderer/components/+namespaces/namespace-details.tsx b/src/renderer/components/+namespaces/namespace-details.tsx index f687630a96..5dfa93cea7 100644 --- a/src/renderer/components/+namespaces/namespace-details.tsx +++ b/src/renderer/components/+namespaces/namespace-details.tsx @@ -12,6 +12,7 @@ import { Spinner } from "../spinner"; import { resourceQuotaStore } from "../+config-resource-quotas/resource-quotas.store"; import { KubeObjectMeta } from "../kube-object/kube-object-meta"; import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry"; +import { limitRangeStore } from "../+config-limit-ranges/limit-ranges.store"; interface Props extends KubeObjectDetailsProps { } @@ -24,8 +25,15 @@ export class NamespaceDetails extends React.Component { return resourceQuotaStore.getAllByNs(namespace); } + @computed get limitranges() { + const namespace = this.props.object.getName(); + + return limitRangeStore.getAllByNs(namespace); + } + componentDidMount() { resourceQuotaStore.loadAll(); + limitRangeStore.loadAll(); } render() { @@ -52,6 +60,16 @@ export class NamespaceDetails extends React.Component { ); })} + + {!this.limitranges && limitRangeStore.isLoading && } + {this.limitranges.map(limitrange => { + return ( + + {limitrange.getName()} + + ); + })} + ); } diff --git a/src/renderer/utils/rbac.ts b/src/renderer/utils/rbac.ts index 5f535c8109..36737ccf3a 100644 --- a/src/renderer/utils/rbac.ts +++ b/src/renderer/utils/rbac.ts @@ -25,4 +25,5 @@ export const ResourceNames: Record = { "horizontalpodautoscalers": "Horizontal Pod Autoscalers", "podsecuritypolicies": "Pod Security Policies", "poddisruptionbudgets": "Pod Disruption Budgets", + "limitranges": "Limit Ranges", };