From 51af8f8aa446c15fe9c49cb10deb5f640d3bced1 Mon Sep 17 00:00:00 2001 From: Piotr Roszatycki Date: Tue, 11 Oct 2022 16:40:23 +0200 Subject: [PATCH] Leases view (#6378) * Leases view Signed-off-by: Piotr Roszatycki * Renamed default exports Signed-off-by: Piotr Roszatycki --- .../config/leases/leases-route.injectable.ts | 25 ++++++ .../leases/navigate-to-leases.injectable.ts | 20 +++++ src/common/k8s-api/endpoints/index.ts | 1 + .../k8s-api/endpoints/lease.api.injectable.ts | 22 +++++ src/common/k8s-api/endpoints/lease.api.ts | 56 ++++++++++++ src/common/rbac.ts | 3 +- .../components/+config-leases/index.ts | 7 ++ .../+config-leases/lease-details.scss | 6 ++ .../+config-leases/lease-details.tsx | 51 +++++++++++ .../leases-route-component.injectable.ts | 21 +++++ .../leases-sidebar-items.injectable.tsx | 38 ++++++++ .../components/+config-leases/leases.scss | 24 +++++ .../components/+config-leases/leases.tsx | 87 +++++++++++++++++++ .../+config-leases/store.injectable.ts | 24 +++++ .../components/+config-leases/store.ts | 10 +++ .../lease-detail-item.injectable.ts | 33 +++++++ src/renderer/utils/rbac.ts | 1 + 17 files changed, 428 insertions(+), 1 deletion(-) create mode 100644 src/common/front-end-routing/routes/cluster/config/leases/leases-route.injectable.ts create mode 100644 src/common/front-end-routing/routes/cluster/config/leases/navigate-to-leases.injectable.ts create mode 100644 src/common/k8s-api/endpoints/lease.api.injectable.ts create mode 100644 src/common/k8s-api/endpoints/lease.api.ts create mode 100644 src/renderer/components/+config-leases/index.ts create mode 100644 src/renderer/components/+config-leases/lease-details.scss create mode 100644 src/renderer/components/+config-leases/lease-details.tsx create mode 100644 src/renderer/components/+config-leases/leases-route-component.injectable.ts create mode 100644 src/renderer/components/+config-leases/leases-sidebar-items.injectable.tsx create mode 100644 src/renderer/components/+config-leases/leases.scss create mode 100644 src/renderer/components/+config-leases/leases.tsx create mode 100644 src/renderer/components/+config-leases/store.injectable.ts create mode 100644 src/renderer/components/+config-leases/store.ts create mode 100644 src/renderer/components/kube-object-details/kube-object-detail-items/implementations/lease-detail-item.injectable.ts diff --git a/src/common/front-end-routing/routes/cluster/config/leases/leases-route.injectable.ts b/src/common/front-end-routing/routes/cluster/config/leases/leases-route.injectable.ts new file mode 100644 index 0000000000..65ee0e3ffa --- /dev/null +++ b/src/common/front-end-routing/routes/cluster/config/leases/leases-route.injectable.ts @@ -0,0 +1,25 @@ +/** + * 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 isAllowedResourceInjectable from "../../../../../utils/is-allowed-resource.injectable"; +import { frontEndRouteInjectionToken } from "../../../../front-end-route-injection-token"; + +const leasesRouteInjectable = getInjectable({ + id: "leases", + + instantiate: (di) => { + const isAllowedResource = di.inject(isAllowedResourceInjectable, "leases"); + + return { + path: "/leases", + clusterFrame: true, + isEnabled: isAllowedResource, + }; + }, + + injectionToken: frontEndRouteInjectionToken, +}); + +export default leasesRouteInjectable; diff --git a/src/common/front-end-routing/routes/cluster/config/leases/navigate-to-leases.injectable.ts b/src/common/front-end-routing/routes/cluster/config/leases/navigate-to-leases.injectable.ts new file mode 100644 index 0000000000..5bf7d74ff1 --- /dev/null +++ b/src/common/front-end-routing/routes/cluster/config/leases/navigate-to-leases.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 leasesRouteInjectable from "./leases-route.injectable"; +import { navigateToRouteInjectionToken } from "../../../../navigate-to-route-injection-token"; + +const navigateToLeasesInjectable = getInjectable({ + id: "navigate-to-leases", + + instantiate: (di) => { + const navigateToRoute = di.inject(navigateToRouteInjectionToken); + const route = di.inject(leasesRouteInjectable); + + return () => navigateToRoute(route); + }, +}); + +export default navigateToLeasesInjectable; diff --git a/src/common/k8s-api/endpoints/index.ts b/src/common/k8s-api/endpoints/index.ts index e13a969faa..c0493e6370 100644 --- a/src/common/k8s-api/endpoints/index.ts +++ b/src/common/k8s-api/endpoints/index.ts @@ -19,6 +19,7 @@ export * from "./events.api"; export * from "./horizontal-pod-autoscaler.api"; export * from "./ingress.api"; export * from "./job.api"; +export * from "./lease.api"; export * from "./limit-range.api"; export * from "./namespace.api"; export * from "./network-policy.api"; diff --git a/src/common/k8s-api/endpoints/lease.api.injectable.ts b/src/common/k8s-api/endpoints/lease.api.injectable.ts new file mode 100644 index 0000000000..41bd5e3935 --- /dev/null +++ b/src/common/k8s-api/endpoints/lease.api.injectable.ts @@ -0,0 +1,22 @@ +/** + * 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 { LeaseApi } from "./lease.api"; +import { kubeApiInjectionToken } from "../kube-api/kube-api-injection-token"; + +const leaseApiInjectable = getInjectable({ + id: "lease-api", + instantiate: (di) => { + assert(di.inject(storesAndApisCanBeCreatedInjectionToken), "leaseApi is only available in certain environments"); + + return new LeaseApi(); + }, + + injectionToken: kubeApiInjectionToken, +}); + +export default leaseApiInjectable; diff --git a/src/common/k8s-api/endpoints/lease.api.ts b/src/common/k8s-api/endpoints/lease.api.ts new file mode 100644 index 0000000000..22cfb778e4 --- /dev/null +++ b/src/common/k8s-api/endpoints/lease.api.ts @@ -0,0 +1,56 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import type { DerivedKubeApiOptions, IgnoredKubeApiOptions } from "../kube-api"; +import { KubeApi } from "../kube-api"; +import type { NamespaceScopedMetadata } from "../kube-object"; +import { KubeObject } from "../kube-object"; + +export interface LeaseSpec { + acquireTime?: string; + holderIdentity: string; + leaseDurationSeconds: number; + leaseTransitions?: number; + renewTime: string; +} + +export class Lease extends KubeObject< + NamespaceScopedMetadata, + void, + LeaseSpec +> { + static readonly kind = "Lease"; + static readonly namespaced = true; + static readonly apiBase = "/apis/coordination.k8s.io/v1/leases"; + + getAcquireTime(): string { + return this.spec.acquireTime || ""; + } + + getHolderIdentity(): string { + return this.spec.holderIdentity; + } + + getLeaseDurationSeconds(): number { + return this.spec.leaseDurationSeconds; + } + + getLeaseTransitions(): number | undefined { + return this.spec.leaseTransitions; + } + + getRenewTime(): string { + return this.spec.renewTime; + } +} + +export class LeaseApi extends KubeApi { + constructor(opts: DerivedKubeApiOptions & IgnoredKubeApiOptions = {}) { + super({ + ...opts, + objectConstructor: Lease, + }); + } +} diff --git a/src/common/rbac.ts b/src/common/rbac.ts index 71c636613a..bfa04ef46b 100644 --- a/src/common/rbac.ts +++ b/src/common/rbac.ts @@ -4,7 +4,7 @@ */ export type KubeResource = - "namespaces" | "nodes" | "events" | "resourcequotas" | "services" | "limitranges" | + "namespaces" | "nodes" | "events" | "resourcequotas" | "services" | "limitranges" | "leases" | "secrets" | "configmaps" | "ingresses" | "networkpolicies" | "persistentvolumeclaims" | "persistentvolumes" | "storageclasses" | "pods" | "daemonsets" | "deployments" | "statefulsets" | "replicasets" | "jobs" | "cronjobs" | "endpoints" | "customresourcedefinitions" | "horizontalpodautoscalers" | "podsecuritypolicies" | "poddisruptionbudgets" | @@ -35,6 +35,7 @@ export const apiResourceRecord: Record = { "jobs": { kind: "Job", group: "batch" }, "namespaces": { kind: "Namespace" }, "limitranges": { kind: "LimitRange" }, + "leases": { kind: "Lease" }, "networkpolicies": { kind: "NetworkPolicy", group: "networking.k8s.io" }, "nodes": { kind: "Node" }, "persistentvolumes": { kind: "PersistentVolume" }, diff --git a/src/renderer/components/+config-leases/index.ts b/src/renderer/components/+config-leases/index.ts new file mode 100644 index 0000000000..dcef127fb2 --- /dev/null +++ b/src/renderer/components/+config-leases/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 "./leases"; +export * from "./lease-details"; diff --git a/src/renderer/components/+config-leases/lease-details.scss b/src/renderer/components/+config-leases/lease-details.scss new file mode 100644 index 0000000000..33628f74dd --- /dev/null +++ b/src/renderer/components/+config-leases/lease-details.scss @@ -0,0 +1,6 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +.LeaseDetails {} \ No newline at end of file diff --git a/src/renderer/components/+config-leases/lease-details.tsx b/src/renderer/components/+config-leases/lease-details.tsx new file mode 100644 index 0000000000..617021edbf --- /dev/null +++ b/src/renderer/components/+config-leases/lease-details.tsx @@ -0,0 +1,51 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import "./lease-details.scss"; + +import React from "react"; +import { observer } from "mobx-react"; +import { DrawerItem } from "../drawer"; +import type { KubeObjectDetailsProps } from "../kube-object-details"; +import { KubeObjectMeta } from "../kube-object-meta"; +import type { Lease } from "../../../common/k8s-api/endpoints"; + +export interface LeaseDetailsProps extends KubeObjectDetailsProps { +} + +@observer +export class LeaseDetails extends React.Component { + + render() { + const { object: lease } = this.props; + + return ( +
+ + + + {lease.getHolderIdentity()} + + + + {lease.getLeaseDurationSeconds()} + + + + + + + + {lease.getRenewTime()} + + +
+ ); + } +} diff --git a/src/renderer/components/+config-leases/leases-route-component.injectable.ts b/src/renderer/components/+config-leases/leases-route-component.injectable.ts new file mode 100644 index 0000000000..ef0ffbf37d --- /dev/null +++ b/src/renderer/components/+config-leases/leases-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 { Leases } from "./leases"; +import leasesRouteInjectable from "../../../common/front-end-routing/routes/cluster/config/leases/leases-route.injectable"; +import { routeSpecificComponentInjectionToken } from "../../routes/route-specific-component-injection-token"; + +const leasesRouteComponentInjectable = getInjectable({ + id: "leases-route-component", + + instantiate: (di) => ({ + route: di.inject(leasesRouteInjectable), + Component: Leases, + }), + + injectionToken: routeSpecificComponentInjectionToken, +}); + +export default leasesRouteComponentInjectable; diff --git a/src/renderer/components/+config-leases/leases-sidebar-items.injectable.tsx b/src/renderer/components/+config-leases/leases-sidebar-items.injectable.tsx new file mode 100644 index 0000000000..178cf8a172 --- /dev/null +++ b/src/renderer/components/+config-leases/leases-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 leasesRouteInjectable from "../../../common/front-end-routing/routes/cluster/config/leases/leases-route.injectable"; +import { configSidebarItemId } from "../+config/config-sidebar-items.injectable"; +import { sidebarItemsInjectionToken } from "../layout/sidebar-items.injectable"; +import routeIsActiveInjectable from "../../routes/route-is-active.injectable"; +import navigateToLeasesInjectable from "../../../common/front-end-routing/routes/cluster/config/leases/navigate-to-leases.injectable"; + +const leasesSidebarItemsInjectable = getInjectable({ + id: "leases-sidebar-items", + + instantiate: (di) => { + const route = di.inject(leasesRouteInjectable); + const navigateToLeases = di.inject(navigateToLeasesInjectable); + const routeIsActive = di.inject(routeIsActiveInjectable, route); + + return computed(() => [ + { + id: "leases", + parentId: configSidebarItemId, + title: "Leases", + onClick: navigateToLeases, + isActive: routeIsActive, + isVisible: route.isEnabled, + orderNumber: 80, + }, + ]); + }, + + injectionToken: sidebarItemsInjectionToken, +}); + +export default leasesSidebarItemsInjectable; diff --git a/src/renderer/components/+config-leases/leases.scss b/src/renderer/components/+config-leases/leases.scss new file mode 100644 index 0000000000..ab7cbce494 --- /dev/null +++ b/src/renderer/components/+config-leases/leases.scss @@ -0,0 +1,24 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +.Leases { + .TableCell { + &.name { + flex: 2; + } + + &.warning { + @include table-cell-warning; + } + + &.keys { + flex: 2.5; + } + + &.age { + flex: .5; + } + } +} \ No newline at end of file diff --git a/src/renderer/components/+config-leases/leases.tsx b/src/renderer/components/+config-leases/leases.tsx new file mode 100644 index 0000000000..1c665762ad --- /dev/null +++ b/src/renderer/components/+config-leases/leases.tsx @@ -0,0 +1,87 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import "./leases.scss"; + +import * as React from "react"; +import { observer } from "mobx-react"; +import type { Lease } from "../../../common/k8s-api/endpoints/lease.api"; +import { KubeObjectStatusIcon } from "../kube-object-status-icon"; +import type { KubeObjectDetailsProps } from "../kube-object-details"; +import { KubeObjectListLayout } from "../kube-object-list-layout"; +import { SiblingsInTabLayout } from "../layout/siblings-in-tab-layout"; +import { KubeObjectAge } from "../kube-object/age"; +import { autoBind } from "../../../common/utils"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import leaseStoreInjectable from "./store.injectable"; +import type { LeaseStore } from "./store"; + +enum columnId { + name = "name", + namespace = "namespace", + holder = "holder", + age = "age", +} + +export interface LeaseProps extends KubeObjectDetailsProps { +} + +interface Dependencies { + leaseStore: LeaseStore; +} + +@observer +class NonInjectedLease extends React.Component { + constructor(props: LeaseProps & Dependencies) { + super(props); + autoBind(this); + } + + render() { + const { leaseStore } = this.props; + + return ( + + lease.getName(), + [columnId.namespace]: lease => lease.getNs(), + [columnId.holder]: lease => lease.getHolderIdentity(), + [columnId.age]: lease => -lease.getCreationTimestamp(), + }} + searchFilters={[ + lease => lease.getSearchFields(), + ]} + renderHeaderTitle="Leases" + renderTableHeader={[ + { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name }, + { className: "warning", showWithColumn: columnId.name }, + { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace }, + { title: "Holder", className: "holder", sortBy: columnId.holder, id: columnId.holder }, + { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, + ]} + renderTableContents={lease => [ + lease.getName(), + , + lease.getNs(), + lease.getHolderIdentity(), + , + ]} + /> + + ); + } +} + +export const Leases = withInjectables(NonInjectedLease, { + getProps: (di, props) => ({ + ...props, + leaseStore: di.inject(leaseStoreInjectable), + }), +}); diff --git a/src/renderer/components/+config-leases/store.injectable.ts b/src/renderer/components/+config-leases/store.injectable.ts new file mode 100644 index 0000000000..b9dda24b07 --- /dev/null +++ b/src/renderer/components/+config-leases/store.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 assert from "assert"; +import { kubeObjectStoreInjectionToken } from "../../../common/k8s-api/api-manager/manager.injectable"; +import leaseApiInjectable from "../../../common/k8s-api/endpoints/lease.api.injectable"; +import storesAndApisCanBeCreatedInjectable from "../../stores-apis-can-be-created.injectable"; +import { LeaseStore } from "./store"; + +const leaseStoreInjectable = getInjectable({ + id: "lease-store", + instantiate: (di) => { + assert(di.inject(storesAndApisCanBeCreatedInjectable), "leaseStore is only available in certain environments"); + + const api = di.inject(leaseApiInjectable); + + return new LeaseStore(api); + }, + injectionToken: kubeObjectStoreInjectionToken, +}); + +export default leaseStoreInjectable; diff --git a/src/renderer/components/+config-leases/store.ts b/src/renderer/components/+config-leases/store.ts new file mode 100644 index 0000000000..db19aed8dd --- /dev/null +++ b/src/renderer/components/+config-leases/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 { Lease, LeaseApi } from "../../../common/k8s-api/endpoints/lease.api"; + +export class LeaseStore extends KubeObjectStore { +} diff --git a/src/renderer/components/kube-object-details/kube-object-detail-items/implementations/lease-detail-item.injectable.ts b/src/renderer/components/kube-object-details/kube-object-detail-items/implementations/lease-detail-item.injectable.ts new file mode 100644 index 0000000000..baaabbf730 --- /dev/null +++ b/src/renderer/components/kube-object-details/kube-object-detail-items/implementations/lease-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 { computed } from "mobx"; +import { LeaseDetails } from "../../../+config-leases"; +import { kubeObjectMatchesToKindAndApiVersion } from "../kube-object-matches-to-kind-and-api-version"; +import currentKubeObjectInDetailsInjectable from "../../current-kube-object-in-details.injectable"; + +const leaseDetailItemInjectable = getInjectable({ + id: "lease-detail-item", + + instantiate: (di) => { + const kubeObject = di.inject(currentKubeObjectInDetailsInjectable); + + return { + Component: LeaseDetails, + enabled: computed(() => isLease(kubeObject.get())), + orderNumber: 10, + }; + }, + + injectionToken: kubeObjectDetailItemInjectionToken, +}); + +const isLease = kubeObjectMatchesToKindAndApiVersion( + "Lease", + ["coordination.k8s.io/v1"], +); + +export default leaseDetailItemInjectable; diff --git a/src/renderer/utils/rbac.ts b/src/renderer/utils/rbac.ts index 9fcee6174c..46219febf6 100644 --- a/src/renderer/utils/rbac.ts +++ b/src/renderer/utils/rbac.ts @@ -10,6 +10,7 @@ export const ResourceNames: Record = { "namespaces": "Namespaces", "nodes": "Nodes", "events": "Events", + "leases": "Leases", "resourcequotas": "Resource Quotas", "services": "Services", "secrets": "Secrets",