From 32320ecb6db5034979df01a2e5096e6b08b68fda Mon Sep 17 00:00:00 2001 From: vshakirova Date: Fri, 30 Jul 2021 13:37:18 +0400 Subject: [PATCH] Rollout support Signed-off-by: vshakirova --- .../endpoints/controller-revision.api.ts | 53 ++++++ .../k8s-api/endpoints/daemon-set.api.ts | 8 +- .../k8s-api/endpoints/deployment.api.ts | 36 ++-- .../k8s-api/endpoints/replica-set.api.ts | 11 ++ .../k8s-api/endpoints/stateful-set.api.ts | 4 +- src/common/routes/controllerrevisions.ts | 31 ++++ .../daemonset-details.tsx | 18 +- .../+workloads-daemonsets/daemonsets.tsx | 13 ++ .../deployment-details.tsx | 8 +- .../deployment-replicasets.scss | 36 ---- .../deployment-revision-history.scss | 64 +++++++ ...ts.tsx => deployment-revision-history.tsx} | 83 ++++----- .../deployment-scale-dialog.test.tsx | 1 + .../deployments.store.ts | 5 + .../+workloads-deployments/deployments.tsx | 28 +-- .../replicaset-scale-dialog.test.tsx | 2 + .../statefulset-details.tsx | 18 +- .../+workloads-statefulsets/statefulsets.tsx | 10 +- .../+workloads/api/workload-kube-api.ts | 50 +++++ .../+workloads/revision-history.scss | 55 ++++++ .../+workloads/revision-history.tsx | 97 ++++++++++ .../components/+workloads/rollout-menu.tsx | 146 +++++++++++++++ .../+workloads/workload-rollback-dialog.scss | 30 +++ .../+workloads/workload-rollback-dialog.tsx | 174 ++++++++++++++++++ src/renderer/components/app.tsx | 2 + .../initializers/kube-object-menu-registry.ts | 8 + 26 files changed, 858 insertions(+), 133 deletions(-) create mode 100644 src/common/k8s-api/endpoints/controller-revision.api.ts create mode 100644 src/common/routes/controllerrevisions.ts delete mode 100644 src/renderer/components/+workloads-deployments/deployment-replicasets.scss create mode 100644 src/renderer/components/+workloads-deployments/deployment-revision-history.scss rename src/renderer/components/+workloads-deployments/{deployment-replicasets.tsx => deployment-revision-history.tsx} (52%) create mode 100644 src/renderer/components/+workloads/api/workload-kube-api.ts create mode 100644 src/renderer/components/+workloads/revision-history.scss create mode 100644 src/renderer/components/+workloads/revision-history.tsx create mode 100644 src/renderer/components/+workloads/rollout-menu.tsx create mode 100644 src/renderer/components/+workloads/workload-rollback-dialog.scss create mode 100644 src/renderer/components/+workloads/workload-rollback-dialog.tsx diff --git a/src/common/k8s-api/endpoints/controller-revision.api.ts b/src/common/k8s-api/endpoints/controller-revision.api.ts new file mode 100644 index 0000000000..d12703faef --- /dev/null +++ b/src/common/k8s-api/endpoints/controller-revision.api.ts @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { WorkloadKubeObject } from "../workload-kube-object"; +import { autoBind } from "../../utils/autobind"; +import { KubeApi } from "../kube-api"; +import type { KubeJsonApiData } from "../kube-json-api"; + +export class ControllerRevisionApi extends KubeApi { + + async getRevisions(params: { namespace: string; name: string }) { + return await this.list({ namespace: params.namespace }, { labelSelector: `name=${params.name}` }); + } +} + +export class ControllerRevision extends WorkloadKubeObject { + static kind = "ControllerRevision"; + static namespaced = true; + static apiBase = "/apis/apps/v1/controllerrevisions"; + + constructor(data: KubeJsonApiData) { + super(data); + autoBind(this); + } + + declare revision: number; + declare data: string; + + getRevisionNumber() { + return this.revision; + } +} + +export const controllerRevisionApi = new ControllerRevisionApi({ + objectConstructor: ControllerRevision, +}); diff --git a/src/common/k8s-api/endpoints/daemon-set.api.ts b/src/common/k8s-api/endpoints/daemon-set.api.ts index 4cd3eff4f4..9a9bf9bac4 100644 --- a/src/common/k8s-api/endpoints/daemon-set.api.ts +++ b/src/common/k8s-api/endpoints/daemon-set.api.ts @@ -22,11 +22,14 @@ import get from "lodash/get"; import { IAffinity, WorkloadKubeObject } from "../workload-kube-object"; import { autoBind } from "../../utils"; -import { KubeApi } from "../kube-api"; import { metricsApi } from "./metrics.api"; import type { KubeJsonApiData } from "../kube-json-api"; import type { IPodContainer, IPodMetrics } from "./pods.api"; import { isClusterPageContext } from "../../utils/cluster-id-url-parsing"; +import { WorkloadKubeApi } from "../../../renderer/components/+workloads/api/workload-kube-api"; + +export class DaemonSetApi extends WorkloadKubeApi { +} export class DaemonSet extends WorkloadKubeObject { static kind = "DaemonSet"; @@ -99,9 +102,6 @@ export class DaemonSet extends WorkloadKubeObject { } } -export class DaemonSetApi extends KubeApi { -} - export function getMetricsForDaemonSets(daemonsets: DaemonSet[], namespace: string, selector = ""): Promise { const podSelector = daemonsets.map(daemonset => `${daemonset.getName()}-[[:alnum:]]{5}`).join("|"); const opts = { category: "pods", pods: podSelector, namespace, selector }; diff --git a/src/common/k8s-api/endpoints/deployment.api.ts b/src/common/k8s-api/endpoints/deployment.api.ts index dd364ab6c2..d10a249efb 100644 --- a/src/common/k8s-api/endpoints/deployment.api.ts +++ b/src/common/k8s-api/endpoints/deployment.api.ts @@ -19,17 +19,15 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import moment from "moment"; - import { IAffinity, WorkloadKubeObject } from "../workload-kube-object"; import { autoBind } from "../../utils"; -import { KubeApi } from "../kube-api"; import { metricsApi } from "./metrics.api"; import type { IPodMetrics } from "./pods.api"; import type { KubeJsonApiData } from "../kube-json-api"; import { isClusterPageContext } from "../../utils/cluster-id-url-parsing"; +import { strategicMergePatch, WorkloadKubeApi } from "../../../renderer/components/+workloads/api/workload-kube-api"; -export class DeploymentApi extends KubeApi { +export class DeploymentApi extends WorkloadKubeApi { protected getScaleApiUrl(params: { namespace: string; name: string }) { return `${this.getUrl(params)}/scale`; } @@ -51,21 +49,32 @@ export class DeploymentApi extends KubeApi { }); } - restart(params: { namespace: string; name: string }) { + pause(params: { namespace: string; name: string }) { return this.request.patch(this.getUrl(params), { data: { spec: { - template: { - metadata: { - annotations: {"kubectl.kubernetes.io/restartedAt" : moment.utc().format()} - } - } + paused: true } } }, { headers: { - "content-type": "application/strategic-merge-patch+json" + "content-type": strategicMergePatch + } + }); + } + + resume(params: { namespace: string; name: string }) { + return this.request.patch(this.getUrl(params), { + data: { + spec: { + paused: false + } + } + }, + { + headers: { + "content-type": strategicMergePatch } }); } @@ -117,6 +126,7 @@ export class Deployment extends WorkloadKubeObject { } declare spec: { + paused?: boolean; replicas: number; selector: { matchLabels: { [app: string]: string } }; template: { @@ -231,6 +241,10 @@ export class Deployment extends WorkloadKubeObject { getReplicas() { return this.spec.replicas || 0; } + + isPaused() { + return this.spec?.paused || false; + } } let deploymentApi: DeploymentApi; diff --git a/src/common/k8s-api/endpoints/replica-set.api.ts b/src/common/k8s-api/endpoints/replica-set.api.ts index eeaf361557..70cf5b8c1e 100644 --- a/src/common/k8s-api/endpoints/replica-set.api.ts +++ b/src/common/k8s-api/endpoints/replica-set.api.ts @@ -66,6 +66,9 @@ export function getMetricsForReplicaSets(replicasets: ReplicaSet[], namespace: s }); } +const changeCauseAnnotation = "kubernetes.io/change-cause"; +const revisionAnnotation = "deployment.kubernetes.io/revision"; + export class ReplicaSet extends WorkloadKubeObject { static kind = "ReplicaSet"; static namespaced = true; @@ -117,6 +120,14 @@ export class ReplicaSet extends WorkloadKubeObject { return this.status.readyReplicas || 0; } + getRevisionNumber(): number { + return parseInt(this.metadata.annotations[revisionAnnotation]); + } + + getChangeCause() { + return this.metadata.annotations[changeCauseAnnotation] || ""; + } + getImages() { const containers: IPodContainer[] = get(this, "spec.template.spec.containers", []); diff --git a/src/common/k8s-api/endpoints/stateful-set.api.ts b/src/common/k8s-api/endpoints/stateful-set.api.ts index 8328ef9bfd..2e8d1cde14 100644 --- a/src/common/k8s-api/endpoints/stateful-set.api.ts +++ b/src/common/k8s-api/endpoints/stateful-set.api.ts @@ -21,13 +21,13 @@ import { IAffinity, WorkloadKubeObject } from "../workload-kube-object"; import { autoBind } from "../../utils"; -import { KubeApi } from "../kube-api"; import { metricsApi } from "./metrics.api"; import type { IPodMetrics } from "./pods.api"; import type { KubeJsonApiData } from "../kube-json-api"; import { isClusterPageContext } from "../../utils/cluster-id-url-parsing"; +import { WorkloadKubeApi } from "../../../renderer/components/+workloads/api/workload-kube-api"; -export class StatefulSetApi extends KubeApi { +export class StatefulSetApi extends WorkloadKubeApi { protected getScaleApiUrl(params: { namespace: string; name: string }) { return `${this.getUrl(params)}/scale`; } diff --git a/src/common/routes/controllerrevisions.ts b/src/common/routes/controllerrevisions.ts new file mode 100644 index 0000000000..6a6d0951c0 --- /dev/null +++ b/src/common/routes/controllerrevisions.ts @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import type { RouteProps } from "react-router"; +import { buildURL } from "../utils/buildUrl"; + +export const controllerRevisionRoute: RouteProps = { + path: "/controllerrevisions" +}; + +export interface ControllerRevisionRouteParams { +} + +export const controllerRevisionURL = buildURL(controllerRevisionRoute.path); diff --git a/src/renderer/components/+workloads-daemonsets/daemonset-details.tsx b/src/renderer/components/+workloads-daemonsets/daemonset-details.tsx index 92615e54ef..5140ca7c39 100644 --- a/src/renderer/components/+workloads-daemonsets/daemonset-details.tsx +++ b/src/renderer/components/+workloads-daemonsets/daemonset-details.tsx @@ -34,12 +34,15 @@ import type { KubeObjectDetailsProps } from "../kube-object-details"; import { DaemonSet, getMetricsForDaemonSets, IPodMetrics } from "../../../common/k8s-api/endpoints"; import { ResourceMetrics, ResourceMetricsText } from "../resource-metrics"; import { PodCharts, podMetricTabs } from "../+workloads-pods/pod-charts"; -import { makeObservable, observable, reaction } from "mobx"; +import { autorun, makeObservable, observable, reaction } from "mobx"; import { PodDetailsList } from "../+workloads-pods/pod-details-list"; import { KubeObjectMeta } from "../kube-object-meta"; import { getActiveClusterEntity } from "../../api/catalog-entity-registry"; import { ClusterMetricsResourceType } from "../../../common/cluster-types"; import { boundMethod } from "../../utils"; +import { RevisionHistory } from "../+workloads/revision-history"; +import type { ControllerRevision } from "../../../common/k8s-api/endpoints/controller-revision.api"; +import { controllerRevisionApi } from "../../../common/k8s-api/endpoints/controller-revision.api"; interface Props extends KubeObjectDetailsProps { } @@ -47,6 +50,7 @@ interface Props extends KubeObjectDetailsProps { @observer export class DaemonSetDetails extends React.Component { @observable metrics: IPodMetrics = null; + @observable revisionHistory: ControllerRevision[] = observable.array(); constructor(props: Props) { super(props); @@ -58,6 +62,17 @@ export class DaemonSetDetails extends React.Component { this.metrics = null; }); + @disposeOnUnmount + revisionLoader = autorun(async () => { + const { object: daemonSet } = this.props; + const revisionHistory = await controllerRevisionApi.getRevisions({ + name: daemonSet.getName(), + namespace: daemonSet.getNs() + }) as ControllerRevision[]; + + this.revisionHistory = observable.array(revisionHistory); + }); + componentDidMount() { podsStore.reloadAll(); } @@ -121,6 +136,7 @@ export class DaemonSetDetails extends React.Component { + ); diff --git a/src/renderer/components/+workloads-daemonsets/daemonsets.tsx b/src/renderer/components/+workloads-daemonsets/daemonsets.tsx index e9c4442b15..a48815187c 100644 --- a/src/renderer/components/+workloads-daemonsets/daemonsets.tsx +++ b/src/renderer/components/+workloads-daemonsets/daemonsets.tsx @@ -33,6 +33,8 @@ import { KubeObjectListLayout } from "../kube-object-list-layout"; import { Badge } from "../badge"; import { KubeObjectStatusIcon } from "../kube-object-status-icon"; import type { DaemonSetsRouteParams } from "../../../common/routes"; +import type { KubeObjectMenuProps } from "../kube-object-menu/kube-object-menu"; +import { RolloutMenu } from "../+workloads/rollout-menu"; enum columnId { name = "name", @@ -91,7 +93,18 @@ export class DaemonSets extends React.Component { this.renderNodeSelector(daemonSet), daemonSet.getAge(), ]} + renderItemMenu={item => } /> ); } } + +export function DaemonSetMenu(props: KubeObjectMenuProps) { + const { object } = props; + + return ( + <> + + + ); +} diff --git a/src/renderer/components/+workloads-deployments/deployment-details.tsx b/src/renderer/components/+workloads-deployments/deployment-details.tsx index 8f4a45a580..547b010de9 100644 --- a/src/renderer/components/+workloads-deployments/deployment-details.tsx +++ b/src/renderer/components/+workloads-deployments/deployment-details.tsx @@ -37,11 +37,10 @@ import { PodCharts, podMetricTabs } from "../+workloads-pods/pod-charts"; import { makeObservable, observable, reaction } from "mobx"; import { PodDetailsList } from "../+workloads-pods/pod-details-list"; import { KubeObjectMeta } from "../kube-object-meta"; -import { replicaSetStore } from "../+workloads-replicasets/replicasets.store"; -import { DeploymentReplicaSets } from "./deployment-replicasets"; import { getActiveClusterEntity } from "../../api/catalog-entity-registry"; import { ClusterMetricsResourceType } from "../../../common/cluster-types"; import { boundMethod } from "../../utils"; +import { DeploymentRevisionHistory } from "./deployment-revision-history"; interface Props extends KubeObjectDetailsProps { } @@ -62,7 +61,6 @@ export class DeploymentDetails extends React.Component { componentDidMount() { podsStore.reloadAll(); - replicaSetStore.reloadAll(); } @boundMethod @@ -80,7 +78,7 @@ export class DeploymentDetails extends React.Component { const nodeSelector = deployment.getNodeSelectors(); const selectors = deployment.getSelectors(); const childPods = deploymentStore.getChildPods(deployment); - const replicaSets = replicaSetStore.getReplicaSetsByOwner(deployment); + const replicaSets = deploymentStore.getRelatedReplicas(deployment); const isMetricHidden = getActiveClusterEntity()?.isMetricHidden(ClusterMetricsResourceType.Deployment); return ( @@ -143,7 +141,7 @@ export class DeploymentDetails extends React.Component { - + ); diff --git a/src/renderer/components/+workloads-deployments/deployment-replicasets.scss b/src/renderer/components/+workloads-deployments/deployment-replicasets.scss deleted file mode 100644 index 0c04f54709..0000000000 --- a/src/renderer/components/+workloads-deployments/deployment-replicasets.scss +++ /dev/null @@ -1,36 +0,0 @@ -.DeploymentDetails { - .ReplicaSets { - position: relative; - min-height: 80px; - - .Table { - margin: 0 (-$margin * 3); - } - - .TableCell { - &:first-child { - margin-left: $margin; - } - - &:last-child { - margin-right: $margin; - } - - &.name { - flex-grow: 2; - } - - &.warning { - @include table-cell-warning; - } - - &.namespace { - flex-grow: 1.2; - } - - &.actions { - @include table-cell-action; - } - } - } -} diff --git a/src/renderer/components/+workloads-deployments/deployment-revision-history.scss b/src/renderer/components/+workloads-deployments/deployment-revision-history.scss new file mode 100644 index 0000000000..b97bb699b9 --- /dev/null +++ b/src/renderer/components/+workloads-deployments/deployment-revision-history.scss @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +.RevisionHistory { + position: relative; + min-height: 80px; + + .Table { + margin: 0 (-$margin * 3); + } + + .TableCell { + &:first-child { + margin-left: $margin; + } + + &:last-child { + margin-right: $margin; + } + + &.revision { + flex-grow: 0.5; + } + + &.name { + flex-grow: 2; + } + + &.warning { + @include table-cell-warning; + } + + &.namespace { + flex-grow: 1.2; + } + + &.actions { + @include table-cell-action; + } + + &.changeCause:empty:after { + content: '—'; + flex-grow: 1.5; + } + } +} diff --git a/src/renderer/components/+workloads-deployments/deployment-replicasets.tsx b/src/renderer/components/+workloads-deployments/deployment-revision-history.tsx similarity index 52% rename from src/renderer/components/+workloads-deployments/deployment-replicasets.tsx rename to src/renderer/components/+workloads-deployments/deployment-revision-history.tsx index e332b19fe8..b042bcfe55 100644 --- a/src/renderer/components/+workloads-deployments/deployment-replicasets.tsx +++ b/src/renderer/components/+workloads-deployments/deployment-revision-history.tsx @@ -19,89 +19,90 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import "./deployment-replicasets.scss"; +import "./deployment-revision-history.scss"; import React from "react"; import { observer } from "mobx-react"; import type { ReplicaSet } from "../../../common/k8s-api/endpoints"; -import { KubeObjectMenu, KubeObjectMenuProps } from "../kube-object-menu"; -import { Spinner } from "../spinner"; -import { prevDefault, stopPropagation } from "../../utils"; import { DrawerTitle } from "../drawer"; import { Table, TableCell, TableHead, TableRow } from "../table"; import { KubeObjectStatusIcon } from "../kube-object-status-icon"; -import { replicaSetStore } from "../+workloads-replicasets/replicasets.store"; -import { showDetails } from "../kube-detail-params"; - +import { prevDefault } from "../../utils/prevDefault"; +import { showDetails } from "../kube-detail-params/params"; +import { Badge } from "../badge/badge"; enum sortBy { name = "name", namespace = "namespace", pods = "pods", age = "age", + revision = "revision", } interface Props { - replicaSets: ReplicaSet[]; + object: ReplicaSet[]; } @observer -export class DeploymentReplicaSets extends React.Component { +export class DeploymentRevisionHistory extends React.Component { private sortingCallbacks = { - [sortBy.name]: (replicaSet: ReplicaSet) => replicaSet.getName(), - [sortBy.namespace]: (replicaSet: ReplicaSet) => replicaSet.getNs(), - [sortBy.age]: (replicaSet: ReplicaSet) => replicaSet.metadata.creationTimestamp, - [sortBy.pods]: (replicaSet: ReplicaSet) => this.getPodsLength(replicaSet), + [sortBy.name]: (object: ReplicaSet) => object.getName(), + [sortBy.namespace]: (object: ReplicaSet) => object.getNs(), + [sortBy.age]: (object: ReplicaSet) => object.metadata.creationTimestamp, + [sortBy.revision]: (object: ReplicaSet) => object.getRevisionNumber(), }; - getPodsLength(replicaSet: ReplicaSet) { - return replicaSetStore.getChildPods(replicaSet).length; - } - render() { - const { replicaSets } = this.props; + const { object } = this.props; - if (!replicaSets.length && !replicaSetStore.isLoaded) return ( -
- ); - if (!replicaSets.length) return null; + if (!object.length) return null; return ( -
- +
+ + Revision Name Namespace - Pods + Change cause Age - { - replicaSets.map(replica => { + object.map(object => { return ( showDetails(replica.selfLink, false))} + onClick={prevDefault(() => showDetails(object.selfLink, false))} > - {replica.getName()} - - {replica.getNs()} - {this.getPodsLength(replica)} - {replica.getAge()} - - + {object.getRevisionNumber()} + {object.getName()} + + {object.getNs()} + {object.getChangeCause() && + +

{object.getChangeCause()}

+ + )} + /> + }
+ {object.getAge()}
); }) @@ -111,9 +112,3 @@ export class DeploymentReplicaSets extends React.Component { ); } } - -export function ReplicaSetMenu(props: KubeObjectMenuProps) { - return ( - - ); -} diff --git a/src/renderer/components/+workloads-deployments/deployment-scale-dialog.test.tsx b/src/renderer/components/+workloads-deployments/deployment-scale-dialog.test.tsx index 22c90dda2c..aa295adbfe 100644 --- a/src/renderer/components/+workloads-deployments/deployment-scale-dialog.test.tsx +++ b/src/renderer/components/+workloads-deployments/deployment-scale-dialog.test.tsx @@ -112,6 +112,7 @@ const dummyDeployment: Deployment = { toPlainObject: jest.fn(), update: jest.fn(), delete: jest.fn(), + isPaused: jest.fn(), }; describe("", () => { diff --git a/src/renderer/components/+workloads-deployments/deployments.store.ts b/src/renderer/components/+workloads-deployments/deployments.store.ts index 64f349491e..8274c0329d 100644 --- a/src/renderer/components/+workloads-deployments/deployments.store.ts +++ b/src/renderer/components/+workloads-deployments/deployments.store.ts @@ -25,6 +25,7 @@ import { apiManager } from "../../../common/k8s-api/api-manager"; import { Deployment, deploymentApi, PodStatus } from "../../../common/k8s-api/endpoints"; import { KubeObjectStore } from "../../../common/k8s-api/kube-object.store"; import { autoBind } from "../../utils"; +import { replicaSetStore } from "../+workloads-replicasets/replicasets.store"; export class DeploymentStore extends KubeObjectStore { api = deploymentApi; @@ -67,6 +68,10 @@ export class DeploymentStore extends KubeObjectStore { .getByLabel(deployment.getTemplateLabels()) .filter(pod => pod.getNs() === deployment.getNs()); } + + getRelatedReplicas(deployment: Deployment) { + return replicaSetStore.getReplicaSetsByOwner(deployment); + } } export const deploymentStore = new DeploymentStore(); diff --git a/src/renderer/components/+workloads-deployments/deployments.tsx b/src/renderer/components/+workloads-deployments/deployments.tsx index 28705fd53d..ed016ee8ef 100644 --- a/src/renderer/components/+workloads-deployments/deployments.tsx +++ b/src/renderer/components/+workloads-deployments/deployments.tsx @@ -24,12 +24,11 @@ import "./deployments.scss"; import React from "react"; import { observer } from "mobx-react"; import type { RouteComponentProps } from "react-router"; -import { Deployment, deploymentApi } from "../../../common/k8s-api/endpoints"; +import type { Deployment } from "../../../common/k8s-api/endpoints"; import type { KubeObjectMenuProps } from "../kube-object-menu"; import { MenuItem } from "../menu"; import { Icon } from "../icon"; import { DeploymentScaleDialog } from "./deployment-scale-dialog"; -import { ConfirmDialog } from "../confirm-dialog"; import { deploymentStore } from "./deployments.store"; import { replicaSetStore } from "../+workloads-replicasets/replicasets.store"; import { podsStore } from "../+workloads-pods/pods.store"; @@ -40,8 +39,8 @@ import { cssNames } from "../../utils"; import kebabCase from "lodash/kebabCase"; import orderBy from "lodash/orderBy"; import { KubeObjectStatusIcon } from "../kube-object-status-icon"; -import { Notifications } from "../notifications"; import type { DeploymentsRouteParams } from "../../../common/routes"; +import { RolloutMenu } from "../+workloads/rollout-menu"; enum columnId { name = "name", @@ -125,28 +124,7 @@ export function DeploymentMenu(props: KubeObjectMenuProps) { Scale - ConfirmDialog.open({ - ok: async () => - { - try { - await deploymentApi.restart({ - namespace: object.getNs(), - name: object.getName(), - }); - } catch (err) { - Notifications.error(err); - } - }, - labelOk: `Restart`, - message: ( -

- Are you sure you want to restart deployment {object.getName()}? -

- ), - })}> - - Restart -
+ ); } diff --git a/src/renderer/components/+workloads-replicasets/replicaset-scale-dialog.test.tsx b/src/renderer/components/+workloads-replicasets/replicaset-scale-dialog.test.tsx index 7f8c31e99c..b98b5ea346 100755 --- a/src/renderer/components/+workloads-replicasets/replicaset-scale-dialog.test.tsx +++ b/src/renderer/components/+workloads-replicasets/replicaset-scale-dialog.test.tsx @@ -107,6 +107,8 @@ const dummyReplicaSet: ReplicaSet = { toPlainObject: jest.fn(), update: jest.fn(), delete: jest.fn(), + getChangeCause: jest.fn(), + getRevisionNumber: jest.fn(), }; describe("", () => { diff --git a/src/renderer/components/+workloads-statefulsets/statefulset-details.tsx b/src/renderer/components/+workloads-statefulsets/statefulset-details.tsx index dc8581dec3..f20dd78b9f 100644 --- a/src/renderer/components/+workloads-statefulsets/statefulset-details.tsx +++ b/src/renderer/components/+workloads-statefulsets/statefulset-details.tsx @@ -23,7 +23,7 @@ import "./statefulset-details.scss"; import React from "react"; import { disposeOnUnmount, observer } from "mobx-react"; -import { makeObservable, observable, reaction } from "mobx"; +import { autorun, makeObservable, observable, reaction } from "mobx"; import { Badge } from "../badge"; import { DrawerItem } from "../drawer"; import { PodDetailsStatuses } from "../+workloads-pods/pod-details-statuses"; @@ -40,6 +40,9 @@ import { KubeObjectMeta } from "../kube-object-meta"; import { getActiveClusterEntity } from "../../api/catalog-entity-registry"; import { ClusterMetricsResourceType } from "../../../common/cluster-types"; import { boundMethod } from "../../utils"; +import { RevisionHistory } from "../+workloads/revision-history"; +import type { ControllerRevision } from "../../../common/k8s-api/endpoints/controller-revision.api"; +import { controllerRevisionApi } from "../../../common/k8s-api/endpoints/controller-revision.api"; interface Props extends KubeObjectDetailsProps { } @@ -47,6 +50,7 @@ interface Props extends KubeObjectDetailsProps { @observer export class StatefulSetDetails extends React.Component { @observable metrics: IPodMetrics = null; + @observable revisionHistory: ControllerRevision[] = observable.array(); constructor(props: Props) { super(props); @@ -58,6 +62,17 @@ export class StatefulSetDetails extends React.Component { this.metrics = null; }); + @disposeOnUnmount + revisionLoader = autorun(async () => { + const { object: statefulSet } = this.props; + const revisionHistory = await controllerRevisionApi.getRevisions({ + name: statefulSet.getName(), + namespace: statefulSet.getNs() + }) as ControllerRevision[]; + + this.revisionHistory = observable.array(revisionHistory); + }); + componentDidMount() { podsStore.reloadAll(); } @@ -119,6 +134,7 @@ export class StatefulSetDetails extends React.Component { + ); diff --git a/src/renderer/components/+workloads-statefulsets/statefulsets.tsx b/src/renderer/components/+workloads-statefulsets/statefulsets.tsx index c3b5b0e023..19f0c31549 100644 --- a/src/renderer/components/+workloads-statefulsets/statefulsets.tsx +++ b/src/renderer/components/+workloads-statefulsets/statefulsets.tsx @@ -32,10 +32,11 @@ import { eventStore } from "../+events/event.store"; import type { KubeObjectMenuProps } from "../kube-object-menu"; import { KubeObjectListLayout } from "../kube-object-list-layout"; import { KubeObjectStatusIcon } from "../kube-object-status-icon"; -import { StatefulSetScaleDialog } from "./statefulset-scale-dialog"; -import { MenuItem } from "../menu/menu"; -import { Icon } from "../icon/icon"; import type { StatefulSetsRouteParams } from "../../../common/routes"; +import { MenuItem } from "../menu/menu"; +import { StatefulSetScaleDialog } from "./statefulset-scale-dialog"; +import { Icon } from "../icon/icon"; +import { RolloutMenu } from "../+workloads/rollout-menu"; enum columnId { name = "name", @@ -89,7 +90,7 @@ export class StatefulSets extends React.Component { , statefulSet.getAge(), ]} - renderItemMenu={item => } + renderItemMenu={item => } /> ); } @@ -104,6 +105,7 @@ export function StatefulSetMenu(props: KubeObjectMenuProps) { Scale + ); } diff --git a/src/renderer/components/+workloads/api/workload-kube-api.ts b/src/renderer/components/+workloads/api/workload-kube-api.ts new file mode 100644 index 0000000000..315a0d7f6f --- /dev/null +++ b/src/renderer/components/+workloads/api/workload-kube-api.ts @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +import moment from "moment"; +import { KubeApi } from "../../../../common/k8s-api/kube-api"; +import type { WorkloadKubeObject } from "../../../../common/k8s-api/workload-kube-object"; + +export const strategicMergePatch = "application/strategic-merge-patch+json"; +export const jsonPatch = "application/json-patch+json"; + +export abstract class WorkloadKubeApi extends KubeApi { + //TODO move scale api + + restart(params: { namespace: string; name: string }) { + return this.request.patch(this.getUrl(params), { + data: { + spec: { + template: { + metadata: { + annotations: { "kubectl.kubernetes.io/restartedAt": moment.utc().format() } + } + } + } + } + }, + { + headers: { + "content-type": strategicMergePatch + } + }); + } +} diff --git a/src/renderer/components/+workloads/revision-history.scss b/src/renderer/components/+workloads/revision-history.scss new file mode 100644 index 0000000000..2f0c42eef1 --- /dev/null +++ b/src/renderer/components/+workloads/revision-history.scss @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +.RevisionHistory { + position: relative; + min-height: 80px; + + .Table { + margin: 0 (-$margin * 3); + } + + .TableCell { + &:first-child { + margin-left: $margin; + } + + &:last-child { + margin-right: $margin; + } + + &.name { + flex-grow: 2; + } + + &.warning { + @include table-cell-warning; + } + + &.namespace { + flex-grow: 1.2; + } + + &.actions { + @include table-cell-action; + } + } +} diff --git a/src/renderer/components/+workloads/revision-history.tsx b/src/renderer/components/+workloads/revision-history.tsx new file mode 100644 index 0000000000..0a3d49af42 --- /dev/null +++ b/src/renderer/components/+workloads/revision-history.tsx @@ -0,0 +1,97 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +import "./revision-history.scss"; + +import React from "react"; +import { observer } from "mobx-react"; +import { DrawerTitle } from "../drawer"; +import { Table, TableCell, TableHead, TableRow } from "../table"; +import { KubeObjectStatusIcon } from "../kube-object-status-icon"; +import type { ControllerRevision } from "../../../common/k8s-api/endpoints/controller-revision.api"; + +enum sortBy { + name = "name", + namespace = "namespace", + pods = "pods", + age = "age", + revision = "revision", +} + +interface Props { + object: ControllerRevision[]; +} + +@observer +export class RevisionHistory extends React.Component { + private sortingCallbacks = { + [sortBy.name]: (object: ControllerRevision) => object.getName(), + [sortBy.namespace]: (object: ControllerRevision) => object.getNs(), + [sortBy.age]: (object: ControllerRevision) => object.metadata.creationTimestamp, + [sortBy.revision]: (object: ControllerRevision) => object.getRevisionNumber(), + }; + + render() { + const { object } = this.props; + + if (!object.length) return null; + + return ( +
+ +
+ + Revision + Name + + Namespace + Age + + { + object.map(object => { + return ( + + {object.getRevisionNumber()} + {object.getName()} + + {object.getNs()} + {object.getAge()} + + ); + }) + } +
+
+ ); + } +} diff --git a/src/renderer/components/+workloads/rollout-menu.tsx b/src/renderer/components/+workloads/rollout-menu.tsx new file mode 100644 index 0000000000..907538f4f8 --- /dev/null +++ b/src/renderer/components/+workloads/rollout-menu.tsx @@ -0,0 +1,146 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +import React from "react"; +import { MenuItem } from "../menu"; +import { ConfirmDialog } from "../confirm-dialog"; +import { Notifications } from "../notifications"; +import { Icon } from "../icon"; +import { WorkloadRollbackDialog } from "./workload-rollback-dialog"; +import type { WorkloadKubeObject } from "../../../common/k8s-api/workload-kube-object"; +import { apiManager } from "../../../common/k8s-api/api-manager"; +import { Deployment, deploymentApi } from "../../../common/k8s-api/endpoints/deployment.api"; + +interface Props { + workloadKubeObject: WorkloadKubeObject; +} + +export function renderRestart(workloadKubeObject: WorkloadKubeObject) { + const api: any = apiManager.getApiByKind(workloadKubeObject.kind, workloadKubeObject.apiVersion); + + return ( + ConfirmDialog.open({ + ok: async () => { + try { + await api.restart({ + namespace: workloadKubeObject.getNs(), + name: workloadKubeObject.getName(), + }); + } catch (err) { + Notifications.error(err); + } + }, + labelOk: `Restart`, + message: ( +

+ Are you sure you want to restart {workloadKubeObject.kind} {workloadKubeObject.getName()}? +

+ ), + })}> + + Restart +
+ ); +} + +export function renderRollback(workloadKubeObject: WorkloadKubeObject) { + + return ( + WorkloadRollbackDialog.open(workloadKubeObject)}> + + Rollback + + ); +} + +export function renderPause(workloadKubeObject: Deployment) { + + return ( + ConfirmDialog.open({ + ok: async () => { + try { + await deploymentApi.pause({ namespace: workloadKubeObject.getNs(), name: workloadKubeObject.getName() }); + } catch (err) { + Notifications.error(err); + } + }, + labelOk: `Pause`, + message: ( +

+ Pause Deployment {workloadKubeObject.getName()}? +

), + })}> + + Pause +
+ ); +} + +export function renderResume(workloadKubeObject: Deployment) { + + return ( + ConfirmDialog.open({ + ok: async () => { + try { + await deploymentApi.resume({ namespace: workloadKubeObject.getNs(), name: workloadKubeObject.getName() }); + } catch (err) { + Notifications.error(err); + } + }, + labelOk: `Resume`, + message: ( +

+ Resume Deployment {workloadKubeObject.getName()}? +

), + })}> + + Resume +
+ ); +} + +export function pauseResumeMenu(workloadKubeObject: Deployment) { + + return ( + <> + {!workloadKubeObject.isPaused() ? renderPause(workloadKubeObject) : renderResume(workloadKubeObject)} + + ); +} + +export class RolloutMenu extends React.Component { + + render() { + const { workloadKubeObject } = this.props; + + if (!workloadKubeObject) { + return null; + } + + return ( + <> + {workloadKubeObject instanceof Deployment && pauseResumeMenu(workloadKubeObject)} + {renderRestart(workloadKubeObject)} + {renderRollback(workloadKubeObject)} + + ); + } +} diff --git a/src/renderer/components/+workloads/workload-rollback-dialog.scss b/src/renderer/components/+workloads/workload-rollback-dialog.scss new file mode 100644 index 0000000000..a7b54e5e03 --- /dev/null +++ b/src/renderer/components/+workloads/workload-rollback-dialog.scss @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +.WorkloadRollbackDialog { + .WizardStep { + text-align: center; + + .step-content { + padding: var(--wizard-spacing); + } + } +} diff --git a/src/renderer/components/+workloads/workload-rollback-dialog.tsx b/src/renderer/components/+workloads/workload-rollback-dialog.tsx new file mode 100644 index 0000000000..6aaf67bc4a --- /dev/null +++ b/src/renderer/components/+workloads/workload-rollback-dialog.tsx @@ -0,0 +1,174 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +import "./workload-rollback-dialog.scss"; + +import type { DialogProps } from "../dialog/dialog"; +import React from "react"; +import { Select, SelectOption } from "../select/select"; +import type { ControllerRevision } from "../../../common/k8s-api/endpoints/controller-revision.api"; +import { Wizard, WizardStep } from "../wizard/wizard"; +import { Dialog } from "../dialog/dialog"; +import { createTerminalTab, TerminalStore } from "../dock/terminal.store"; +import type { WorkloadKubeObject } from "../../../common/k8s-api/workload-kube-object"; +import type { ReplicaSet } from "../../../common/k8s-api/endpoints/replica-set.api"; +import { makeObservable, observable } from "mobx"; +import { observer } from "mobx-react"; +import orderBy from "lodash/orderBy"; +import { Deployment } from "../../../common/k8s-api/endpoints/deployment.api"; +import { deploymentStore } from "../+workloads-deployments/deployments.store"; +import { controllerRevisionApi } from "../../../common/k8s-api/endpoints/controller-revision.api"; + +interface Props extends DialogProps { +} + +const dialogState = observable.object({ + isOpen: false, + workloadKubeObject: null as WorkloadKubeObject, +}); + +export interface KubeObjectHistory { + revision: number; + date: string; + name: string; +} + +@observer +export class WorkloadRollbackDialog extends React.Component { + + @observable workloadObject: WorkloadKubeObject; + @observable isLoading = false; + @observable revisions = observable.array(); + @observable revision: KubeObjectHistory; + + constructor(props: Props) { + super(props); + makeObservable(this); + } + + static open(workloadKubeObject: WorkloadKubeObject) { + dialogState.isOpen = true; + dialogState.workloadKubeObject = workloadKubeObject; + } + + static close() { + dialogState.isOpen = false; + } + + close = () => { + WorkloadRollbackDialog.close(); + }; + + get workloadKubeObject(): WorkloadKubeObject { + return dialogState.workloadKubeObject; + } + + getHistory(revisions: ReplicaSet[] | ControllerRevision[]) { + return revisions.map(revision => ({ + revision: revision.getRevisionNumber(), + date: new Date(revision.metadata.creationTimestamp).toLocaleString(), + name: revision.getName(), + } as KubeObjectHistory)); + } + + onOpen = async () => { + this.isLoading = true; + let revisions: ReplicaSet[] | ControllerRevision[]; + + if(this.workloadKubeObject instanceof Deployment) { + revisions = deploymentStore.getRelatedReplicas(this.workloadKubeObject); + } + else { + revisions = await controllerRevisionApi.getRevisions({ namespace: this.workloadKubeObject.getNs(), + name: this.workloadKubeObject.getName() }); + } + + this.revisions = observable.array(orderBy(this.getHistory(revisions), "revision", "desc")); + this.revision = this.revisions[0]; + this.isLoading = false; + }; + + rollback = async () => { + const shell = createTerminalTab({ + title: `Rollback: ${this.workloadKubeObject.getName()} (namespace: ${this.workloadKubeObject.getNs()})` + }); + + TerminalStore.getInstance().sendCommand(`kubectl rollout undo ${this.workloadKubeObject.kind.toLocaleLowerCase()}` + + `/${this.workloadKubeObject.getName()} --to-revision=${this.revision.revision} -n=${this.workloadKubeObject.getNs()}`, + { + enter: true, + tabId: shell.id + }); + + this.close(); + }; + + renderContent() { + const { revision, revisions } = this; + + if (revisions.length < 2) { + return

No revisions to rollback.

; + } + + return ( +
+ Revision +