From d143b234b750cbee9d1ea6fb0639888204f3a6f1 Mon Sep 17 00:00:00 2001 From: Violetta <38247153+vshakirova@users.noreply.github.com> Date: Thu, 10 Dec 2020 16:23:51 +0400 Subject: [PATCH] Full support for ReplicaSets (#1704) Signed-off-by: vshakirova --- integration/__tests__/app.tests.ts | 6 + locales/en/messages.po | 24 +++ locales/fi/messages.po | 24 +++ locales/ru/messages.po | 24 +++ src/common/rbac.ts | 1 + src/renderer/api/endpoints/pods.api.ts | 65 ++++--- src/renderer/api/endpoints/replica-set.api.ts | 85 ++++++--- .../deployment-details.tsx | 8 - .../overview-statuses.scss | 2 +- .../+workloads-overview/overview-statuses.tsx | 1 + .../replicaset-scale-dialog.scss | 49 +++++ .../replicaset-scale-dialog.test.tsx | 167 +++++++++++++++++ .../replicaset-scale-dialog.tsx | 169 ++++++++++++++++++ .../+workloads-replicasets/replicasets.scss | 25 +-- .../replicasets.store.ts | 21 +++ .../+workloads-replicasets/replicasets.tsx | 136 +++++++------- .../components/+workloads/workloads.route.ts | 8 + .../components/+workloads/workloads.stores.ts | 2 + .../components/+workloads/workloads.tsx | 12 +- src/renderer/components/app.tsx | 2 + 20 files changed, 677 insertions(+), 154 deletions(-) create mode 100644 src/renderer/components/+workloads-replicasets/replicaset-scale-dialog.scss create mode 100755 src/renderer/components/+workloads-replicasets/replicaset-scale-dialog.test.tsx create mode 100644 src/renderer/components/+workloads-replicasets/replicaset-scale-dialog.tsx diff --git a/integration/__tests__/app.tests.ts b/integration/__tests__/app.tests.ts index 8672076226..285af70ceb 100644 --- a/integration/__tests__/app.tests.ts +++ b/integration/__tests__/app.tests.ts @@ -273,6 +273,12 @@ describe("Lens integration tests", () => { expectedSelector: "h5.title", expectedText: "Stateful Sets" }, + { + name: "ReplicaSets", + href: "replicasets", + expectedSelector: "h5.title", + expectedText: "Replica Sets" + }, { name: "Jobs", href: "jobs", diff --git a/locales/en/messages.po b/locales/en/messages.po index 53e5d40d09..ec9ac00f25 100644 --- a/locales/en/messages.po +++ b/locales/en/messages.po @@ -223,6 +223,7 @@ msgstr "Affinities" #: src/renderer/components/+workloads-pods/pods.tsx:78 #: src/renderer/components/+workloads-replicasets/replicasets.tsx:51 #: src/renderer/components/+workloads-statefulsets/statefulsets.tsx:41 +#: src/renderer/components/+workloads-replicasets/replicasets.tsx:56 msgid "Age" msgstr "Age" @@ -766,6 +767,10 @@ msgstr "Cron Jobs" msgid "CronJobs" msgstr "CronJobs" +#: src/renderer/components/+workloads-replicasets/replicasets.tsx:54 +msgid "Current" +msgstr "Current" + #: src/renderer/components/+config-autoscalers/hpa-details.tsx:50 msgid "Current / Target" msgstr "Current / Target" @@ -777,6 +782,7 @@ msgstr "Current Healthy" #: src/renderer/components/+workloads-statefulsets/statefulset-scale-dialog.tsx:101 #: src/renderer/components/+workloads-deployments/deployment-scale-dialog.tsx:103 +#: src/renderer/components/+workloads-replicasets/replicaset-scale-dialog.tsx:104 msgid "Current replica scale: {currentReplicas}" msgstr "Current replica scale: {currentReplicas}" @@ -861,6 +867,10 @@ msgstr "Deployments" msgid "Description" msgstr "Description" +#: src/renderer/components/+workloads-replicasets/replicasets.tsx:53 +msgid "Desired" +msgstr "Desired" + #: src/renderer/components/+config-pod-disruption-budgets/pod-disruption-budgets-details.tsx:42 #: src/renderer/components/+config-pod-disruption-budgets/pod-disruption-budgets.tsx:41 msgid "Desired Healthy" @@ -868,6 +878,7 @@ msgstr "Desired Healthy" #: src/renderer/components/+workloads-statefulsets/statefulset-scale-dialog.tsx:105 #: src/renderer/components/+workloads-deployments/deployment-scale-dialog.tsx:107 +#: src/renderer/components/+workloads-replicasets/replicaset-scale-dialog.tsx:108 msgid "Desired number of replicas" msgstr "Desired number of replicas" @@ -1132,6 +1143,7 @@ msgstr "Hide" #: src/renderer/components/+workloads-statefulsets/statefulset-scale-dialog.tsx:127 #: src/renderer/components/+workloads-deployments/deployment-scale-dialog.tsx:116 +#: src/renderer/components/+workloads-replicasets/replicaset-scale-dialog.tsx:131 msgid "High number of replicas may cause cluster performance issues" msgstr "High number of replicas may cause cluster performance issues" @@ -1583,6 +1595,7 @@ msgstr "Mounts" #: src/renderer/components/+workspaces/workspaces.tsx:135 #: src/renderer/components/dock/edit-resource.tsx:89 #: src/renderer/components/kube-object/kube-object-meta.tsx:20 +#: src/renderer/components/+workloads-replicasets/replicasets.tsx:50 msgid "Name" msgstr "Name" @@ -1632,6 +1645,7 @@ msgstr "Names" #: src/renderer/components/item-object-list/page-filters-select.tsx:57 #: src/renderer/components/kube-object/kube-object-meta.tsx:23 #: src/renderer/components/+workloads-pods/pod-details-list.tsx:144 +#: src/renderer/components/+workloads-replicasets/replicasets.tsx:52 msgid "Namespace" msgstr "Namespace" @@ -2041,6 +2055,7 @@ msgstr "Read-only Root Filesystem" msgid "Readiness" msgstr "Readiness" +#: src/renderer/components/+workloads-replicasets/replicasets.tsx:55 #: src/renderer/components/+workloads-pods/pod-details-list.tsx:145 msgid "Ready" msgstr "Ready" @@ -2156,6 +2171,10 @@ msgstr "Removing helm branch <0>{0} has failed: {1}" msgid "Replicas" msgstr "Replicas" +#: src/renderer/components/+workloads/workloads.tsx:70 +msgid "ReplicaSets" +msgstr "ReplicaSets" + #: src/renderer/components/dock/install-chart.tsx:119 msgid "Repo/Name" msgstr "Repo/Name" @@ -2347,6 +2366,7 @@ msgstr "Save" #: src/renderer/components/+workloads-deployments/deployment-scale-dialog.tsx:128 #: src/renderer/components/+workloads-deployments/deployments.tsx:83 #: src/renderer/components/+workloads-deployments/deployments.tsx:84 +#: src/renderer/components/+workloads-replicasets/replicaset-scale-dialog.tsx:160 msgid "Scale" msgstr "Scale" @@ -2354,6 +2374,10 @@ msgstr "Scale" msgid "Scale Deployment <0>{deploymentName}" msgstr "Scale Deployment <0>{deploymentName}" +#: src/renderer/components/+workloads-replicasets/replicaset-scale-dialog.tsx:143 +msgid "Scale Replica Set <0>{replicaSetName}" +msgstr "Scale Replica Set <0>{replicaSetName}" + #: src/renderer/components/+workloads-statefulsets/statefulset-scale-dialog.tsx:139 msgid "Scale Stateful Set <0>{statefulSetName}" msgstr "Scale Stateful Set <0>{statefulSetName}" diff --git a/locales/fi/messages.po b/locales/fi/messages.po index 6524225d19..02995c74db 100644 --- a/locales/fi/messages.po +++ b/locales/fi/messages.po @@ -222,6 +222,7 @@ msgstr "" #: src/renderer/components/+workloads-pods/pods.tsx:78 #: src/renderer/components/+workloads-replicasets/replicasets.tsx:51 #: src/renderer/components/+workloads-statefulsets/statefulsets.tsx:41 +#: src/renderer/components/+workloads-replicasets/replicasets.tsx:56 msgid "Age" msgstr "" @@ -761,6 +762,10 @@ msgstr "" msgid "CronJobs" msgstr "" +#: src/renderer/components/+workloads-replicasets/replicasets.tsx:54 +msgid "Current" +msgstr "" + #: src/renderer/components/+config-autoscalers/hpa-details.tsx:50 msgid "Current / Target" msgstr "" @@ -772,6 +777,7 @@ msgstr "" #: src/renderer/components/+workloads-statefulsets/statefulset-scale-dialog.tsx:101 #: src/renderer/components/+workloads-deployments/deployment-scale-dialog.tsx:103 +#: src/renderer/components/+workloads-replicasets/replicaset-scale-dialog.tsx:104 msgid "Current replica scale: {currentReplicas}" msgstr "" @@ -856,6 +862,10 @@ msgstr "" msgid "Description" msgstr "" +#: src/renderer/components/+workloads-replicasets/replicasets.tsx:53 +msgid "Desired" +msgstr "" + #: src/renderer/components/+config-pod-disruption-budgets/pod-disruption-budgets-details.tsx:42 #: src/renderer/components/+config-pod-disruption-budgets/pod-disruption-budgets.tsx:41 msgid "Desired Healthy" @@ -863,6 +873,7 @@ msgstr "" #: src/renderer/components/+workloads-statefulsets/statefulset-scale-dialog.tsx:105 #: src/renderer/components/+workloads-deployments/deployment-scale-dialog.tsx:107 +#: src/renderer/components/+workloads-replicasets/replicaset-scale-dialog.tsx:108 msgid "Desired number of replicas" msgstr "" @@ -1122,6 +1133,7 @@ msgstr "" #: src/renderer/components/+workloads-statefulsets/statefulset-scale-dialog.tsx:127 #: src/renderer/components/+workloads-deployments/deployment-scale-dialog.tsx:116 +#: src/renderer/components/+workloads-replicaset/replicaset-scale-dialog.tsx:131 msgid "High number of replicas may cause cluster performance issues" msgstr "" @@ -1573,6 +1585,7 @@ msgstr "" #: src/renderer/components/+workspaces/workspaces.tsx:135 #: src/renderer/components/dock/edit-resource.tsx:89 #: src/renderer/components/kube-object/kube-object-meta.tsx:20 +#: src/renderer/components/+workloads-replicasets/replicasets.tsx:50 msgid "Name" msgstr "" @@ -1622,6 +1635,7 @@ msgstr "" #: src/renderer/components/item-object-list/page-filters-select.tsx:57 #: src/renderer/components/kube-object/kube-object-meta.tsx:23 #: src/renderer/components/+workloads-pods/pod-details-list.tsx:144 +#: src/renderer/components/+workloads-replicasets/replicasets.tsx:52 msgid "Namespace" msgstr "" @@ -2023,6 +2037,7 @@ msgstr "" msgid "Readiness" msgstr "" +#: src/renderer/components/+workloads-replicasets/replicasets.tsx:55 #: src/renderer/components/+workloads-pods/pod-details-list.tsx:145 msgid "Ready" msgstr "" @@ -2138,6 +2153,10 @@ msgstr "" msgid "Replicas" msgstr "" +#: src/renderer/components/+workloads/workloads.tsx:70 +msgid "ReplicaSets" +msgstr "" + #: src/renderer/components/dock/install-chart.tsx:119 msgid "Repo/Name" msgstr "" @@ -2329,6 +2348,7 @@ msgstr "" #: src/renderer/components/+workloads-deployments/deployment-scale-dialog.tsx:128 #: src/renderer/components/+workloads-deployments/deployments.tsx:83 #: src/renderer/components/+workloads-deployments/deployments.tsx:84 +#: src/renderer/components/+workloads-replicasets/replicaset-scale-dialog.tsx:160 msgid "Scale" msgstr "" @@ -2336,6 +2356,10 @@ msgstr "" msgid "Scale Deployment <0>{deploymentName}" msgstr "" +#: src/renderer/components/+workloads-replicasets/replicaset-scale-dialog.tsx:143 +msgid "Scale Replica Set <0>{replicaSetName}" +msgstr "" + #: src/renderer/components/+workloads-statefulsets/statefulset-scale-dialog.tsx:139 msgid "Scale Stateful Set <0>{statefulSetName}" msgstr "" diff --git a/locales/ru/messages.po b/locales/ru/messages.po index fe45943fed..0f5fb2f9e2 100644 --- a/locales/ru/messages.po +++ b/locales/ru/messages.po @@ -223,6 +223,7 @@ msgstr "Аффинитеты" #: src/renderer/components/+workloads-pods/pods.tsx:78 #: src/renderer/components/+workloads-replicasets/replicasets.tsx:51 #: src/renderer/components/+workloads-statefulsets/statefulsets.tsx:41 +#: src/renderer/components/+workloads-replicasets/replicasets.tsx:56 msgid "Age" msgstr "Возраст" @@ -766,6 +767,10 @@ msgstr "" msgid "CronJobs" msgstr "CronJobs" +#: src/renderer/components/+workloads-replicasets/replicasets.tsx:54 +msgid "Current" +msgstr "Текущее" + #: src/renderer/components/+config-autoscalers/hpa-details.tsx:50 msgid "Current / Target" msgstr "Текущее / Цель" @@ -777,6 +782,7 @@ msgstr "" #: src/renderer/components/+workloads-statefulsets/statefulset-scale-dialog.tsx:101 #: src/renderer/components/+workloads-deployments/deployment-scale-dialog.tsx:103 +#: src/renderer/components/+workloads-replicasets/replicaset-scale-dialog.tsx:104 msgid "Current replica scale: {currentReplicas}" msgstr "Текущий размер реплики: {currentReplicas}" @@ -861,6 +867,10 @@ msgstr "Deployments" msgid "Description" msgstr "Описание" +#: src/renderer/components/+workloads-replicasets/replicasets.tsx:53 +msgid "Desired" +msgstr "Желаемое" + #: src/renderer/components/+config-pod-disruption-budgets/pod-disruption-budgets-details.tsx:42 #: src/renderer/components/+config-pod-disruption-budgets/pod-disruption-budgets.tsx:41 msgid "Desired Healthy" @@ -868,6 +878,7 @@ msgstr "" #: src/renderer/components/+workloads-statefulsets/statefulset-scale-dialog.tsx:105 #: src/renderer/components/+workloads-deployments/deployment-scale-dialog.tsx:107 +#: src/renderer/components/+workloads-replicasets/replicaset-scale-dialog.tsx:108 msgid "Desired number of replicas" msgstr "Нужный уровень реплик" @@ -1132,6 +1143,7 @@ msgstr "Скрыть" #: src/renderer/components/+workloads-statefulsets/statefulset-scale-dialog.tsx:127 #: src/renderer/components/+workloads-deployments/deployment-scale-dialog.tsx:116 +#: src/renderer/components/+workloads-replicasets/replicaset-scale-dialog.tsx:131 msgid "High number of replicas may cause cluster performance issues" msgstr "Большое количество реплик может вызвать проблемы с производительностью кластера" @@ -1583,6 +1595,7 @@ msgstr "Установки" #: src/renderer/components/+workspaces/workspaces.tsx:135 #: src/renderer/components/dock/edit-resource.tsx:89 #: src/renderer/components/kube-object/kube-object-meta.tsx:20 +#: src/renderer/components/+workloads-replicasets/replicasets.tsx:50 msgid "Name" msgstr "Имя" @@ -1632,6 +1645,7 @@ msgstr "" #: src/renderer/components/item-object-list/page-filters-select.tsx:57 #: src/renderer/components/kube-object/kube-object-meta.tsx:23 #: src/renderer/components/+workloads-pods/pod-details-list.tsx:144 +#: src/renderer/components/+workloads-replicasets/replicasets.tsx:52 msgid "Namespace" msgstr "Namespace" @@ -2041,6 +2055,7 @@ msgstr "" msgid "Readiness" msgstr "Готовность" +#: src/renderer/components/+workloads-replicasets/replicasets.tsx:55 #: src/renderer/components/+workloads-pods/pod-details-list.tsx:145 msgid "Ready" msgstr "Готовы" @@ -2156,6 +2171,10 @@ msgstr "" msgid "Replicas" msgstr "Реплики" +#: src/renderer/components/+workloads/workloads.tsx:70 +msgid "ReplicaSets" +msgstr "ReplicaSets" + #: src/renderer/components/dock/install-chart.tsx:119 msgid "Repo/Name" msgstr "Репозиторий/Имя" @@ -2347,6 +2366,7 @@ msgstr "Сохранить" #: src/renderer/components/+workloads-deployments/deployment-scale-dialog.tsx:128 #: src/renderer/components/+workloads-deployments/deployments.tsx:83 #: src/renderer/components/+workloads-deployments/deployments.tsx:84 +#: src/renderer/components/+workloads-replicasets/replicaset-scale-dialog.tsx:160 msgid "Scale" msgstr "Масштабировать" @@ -2354,6 +2374,10 @@ msgstr "Масштабировать" msgid "Scale Deployment <0>{deploymentName}" msgstr "Масштабировать Deployment <0>{deploymentName}" +#: src/renderer/components/+workloads-replicasets/replicaset-scale-dialog.tsx:143 +msgid "Scale Replica Set <0>{replicaSetName}" +msgstr "Масштабировать Replica Set <0>{replicaSetName}" + #: src/renderer/components/+workloads-statefulsets/statefulset-scale-dialog.tsx:139 msgid "Scale Stateful Set <0>{statefulSetName}" msgstr "Масштабировать Stateful Set <0>{statefulSetName}" diff --git a/src/common/rbac.ts b/src/common/rbac.ts index 702d87d394..0e0e5a780e 100644 --- a/src/common/rbac.ts +++ b/src/common/rbac.ts @@ -31,6 +31,7 @@ export const apiResources: KubeApiResource[] = [ { resource: "poddisruptionbudgets" }, { resource: "podsecuritypolicies" }, { resource: "resourcequotas" }, + { resource: "replicasets", group: "apps" }, { resource: "secrets" }, { resource: "services" }, { resource: "statefulsets", group: "apps" }, diff --git a/src/renderer/api/endpoints/pods.api.ts b/src/renderer/api/endpoints/pods.api.ts index 11b581db8f..447503558b 100644 --- a/src/renderer/api/endpoints/pods.api.ts +++ b/src/renderer/api/endpoints/pods.api.ts @@ -67,7 +67,7 @@ export interface IPodContainer { image: string; command?: string[]; args?: string[]; - ports: { + ports?: { name?: string; containerPort: number; protocol: string; @@ -137,7 +137,7 @@ interface IContainerProbe { export interface IPodContainerStatus { name: string; - state: { + state?: { [index: string]: object; running?: { startedAt: string; @@ -153,21 +153,28 @@ export interface IPodContainerStatus { reason: string; }; }; - lastState: { + lastState?: { [index: string]: object; + running?: { + startedAt: string; + }; + waiting?: { + reason: string; + message: string; + }; terminated?: { startedAt: string; finishedAt: string; exitCode: number; reason: string; - containerID: string; }; }; ready: boolean; restartCount: number; image: string; imageID: string; - containerID: string; + containerID?: string; + started?: boolean; } @autobind() @@ -196,28 +203,44 @@ export class Pod extends WorkloadKubeObject { }[]; initContainers: IPodContainer[]; containers: IPodContainer[]; - restartPolicy: string; - terminationGracePeriodSeconds: number; - dnsPolicy: string; + restartPolicy?: string; + terminationGracePeriodSeconds?: number; + activeDeadlineSeconds?: number; + dnsPolicy?: string; serviceAccountName: string; serviceAccount: string; - priority: number; - priorityClassName: string; - nodeName: string; + automountServiceAccountToken?: boolean; + priority?: number; + priorityClassName?: string; + nodeName?: string; nodeSelector?: { [selector: string]: string; }; - securityContext: {}; - schedulerName: string; - tolerations: { - key: string; - operator: string; - effect: string; - tolerationSeconds: number; + securityContext?: {}; + imagePullSecrets?: { + name: string; }[]; - affinity: IAffinity; + hostNetwork?: boolean; + hostPID?: boolean; + hostIPC?: boolean; + shareProcessNamespace?: boolean; + hostname?: string; + subdomain?: string; + schedulerName?: string; + tolerations?: { + key?: string; + operator?: string; + effect?: string; + tolerationSeconds?: number; + value?: string; + }[]; + hostAliases?: { + ip: string; + hostnames: string[]; + }; + affinity?: IAffinity; }; - status: { + status?: { phase: string; conditions: { type: string; @@ -230,7 +253,7 @@ export class Pod extends WorkloadKubeObject { startTime: string; initContainerStatuses?: IPodContainerStatus[]; containerStatuses?: IPodContainerStatus[]; - qosClass: string; + qosClass?: string; reason?: string; }; diff --git a/src/renderer/api/endpoints/replica-set.api.ts b/src/renderer/api/endpoints/replica-set.api.ts index 999de8c1ac..eb1131f645 100644 --- a/src/renderer/api/endpoints/replica-set.api.ts +++ b/src/renderer/api/endpoints/replica-set.api.ts @@ -1,51 +1,78 @@ import get from "lodash/get"; import { autobind } from "../../utils"; -import { IAffinity, WorkloadKubeObject } from "../workload-kube-object"; -import { IPodContainer } from "./pods.api"; +import { WorkloadKubeObject } from "../workload-kube-object"; +import { IPodContainer, Pod } from "./pods.api"; import { KubeApi } from "../kube-api"; +export class ReplicaSetApi extends KubeApi { + protected getScaleApiUrl(params: { namespace: string; name: string }) { + return `${this.getUrl(params)}/scale`; + } + + getReplicas(params: { namespace: string; name: string }): Promise { + return this.request + .get(this.getScaleApiUrl(params)) + .then(({ status }: any) => status?.replicas); + } + + scale(params: { namespace: string; name: string }, replicas: number) { + return this.request.put(this.getScaleApiUrl(params), { + data: { + metadata: params, + spec: { + replicas + } + } + }); + } +} + @autobind() export class ReplicaSet extends WorkloadKubeObject { static kind = "ReplicaSet"; static namespaced = true; static apiBase = "/apis/apps/v1/replicasets"; - spec: { replicas?: number; - selector?: { - matchLabels: { - [key: string]: string; - }; - }; - containers?: IPodContainer[]; + selector: { matchLabels: { [app: string]: string } }; template?: { - spec?: { - affinity?: IAffinity; - nodeSelector?: { - [selector: string]: string; + metadata: { + labels: { + app: string; }; - tolerations: { - key: string; - operator: string; - effect: string; - tolerationSeconds: number; - }[]; - containers: IPodContainer[]; }; + spec?: Pod["spec"]; }; - restartPolicy?: string; - terminationGracePeriodSeconds?: number; - dnsPolicy?: string; - schedulerName?: string; + minReadySeconds?: number; }; status: { replicas: number; - fullyLabeledReplicas: number; - readyReplicas: number; - availableReplicas: number; - observedGeneration: number; + fullyLabeledReplicas?: number; + readyReplicas?: number; + availableReplicas?: number; + observedGeneration?: number; + conditions?: { + type: string; + status: string; + lastUpdateTime: string; + lastTransitionTime: string; + reason: string; + message: string; + }[]; }; + getDesired() { + return this.spec.replicas || 0; + } + + getCurrent() { + return this.status.availableReplicas || 0; + } + + getReady() { + return this.status.readyReplicas || 0; + } + getImages() { const containers: IPodContainer[] = get(this, "spec.template.spec.containers", []); @@ -53,6 +80,6 @@ export class ReplicaSet extends WorkloadKubeObject { } } -export const replicaSetApi = new KubeApi({ +export const replicaSetApi = new ReplicaSetApi({ objectConstructor: ReplicaSet, }); diff --git a/src/renderer/components/+workloads-deployments/deployment-details.tsx b/src/renderer/components/+workloads-deployments/deployment-details.tsx index bd2f10755a..a06b1c4d80 100644 --- a/src/renderer/components/+workloads-deployments/deployment-details.tsx +++ b/src/renderer/components/+workloads-deployments/deployment-details.tsx @@ -11,7 +11,6 @@ import { cssNames } from "../../utils"; import { PodDetailsTolerations } from "../+workloads-pods/pod-details-tolerations"; import { PodDetailsAffinities } from "../+workloads-pods/pod-details-affinities"; import { KubeEventDetails } from "../+events/kube-event-details"; -import { replicaSetStore } from "../+workloads-replicasets/replicasets.store"; import { podsStore } from "../+workloads-pods/pods.store"; import { KubeObjectDetailsProps } from "../kube-object"; import { _i18n } from "../../i18n"; @@ -20,7 +19,6 @@ import { deploymentStore } from "./deployments.store"; import { PodCharts, podMetricTabs } from "../+workloads-pods/pod-charts"; import { reaction } from "mobx"; import { PodDetailsList } from "../+workloads-pods/pod-details-list"; -import { ReplicaSets } from "../+workloads-replicasets"; import { KubeObjectMeta } from "../kube-object/kube-object-meta"; import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry"; @@ -38,10 +36,6 @@ export class DeploymentDetails extends React.Component { if (!podsStore.isLoaded) { podsStore.loadAll(); } - - if (!replicaSetStore.isLoaded) { - replicaSetStore.loadAll(); - } } componentWillUnmount() { @@ -56,7 +50,6 @@ 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 metrics = deploymentStore.metrics; return ( @@ -118,7 +111,6 @@ export class DeploymentDetails extends React.Component { - ); diff --git a/src/renderer/components/+workloads-overview/overview-statuses.scss b/src/renderer/components/+workloads-overview/overview-statuses.scss index 21e6e17cfb..e9d063eb53 100644 --- a/src/renderer/components/+workloads-overview/overview-statuses.scss +++ b/src/renderer/components/+workloads-overview/overview-statuses.scss @@ -15,7 +15,7 @@ .workloads { display: grid; - grid-template-columns: repeat(auto-fit, 155px); + grid-template-columns: repeat(auto-fit, 180px); justify-content: space-evenly; grid-gap: $margin; padding: $padding * 2; diff --git a/src/renderer/components/+workloads-overview/overview-statuses.tsx b/src/renderer/components/+workloads-overview/overview-statuses.tsx index e514d3b485..68dcf646ea 100644 --- a/src/renderer/components/+workloads-overview/overview-statuses.tsx +++ b/src/renderer/components/+workloads-overview/overview-statuses.tsx @@ -18,6 +18,7 @@ const resources: KubeResource[] = [ "deployments", "statefulsets", "daemonsets", + "replicasets", "jobs", "cronjobs", ]; diff --git a/src/renderer/components/+workloads-replicasets/replicaset-scale-dialog.scss b/src/renderer/components/+workloads-replicasets/replicaset-scale-dialog.scss new file mode 100644 index 0000000000..98f6422891 --- /dev/null +++ b/src/renderer/components/+workloads-replicasets/replicaset-scale-dialog.scss @@ -0,0 +1,49 @@ +.ReplicaSetScaleDialog { + .Wizard { + .header { + span { + color: #a0a0a0; + white-space: nowrap; + text-overflow: ellipsis; + } + } + + .WizardStep { + .step-content { + min-height: 90px; + overflow: hidden; + } + } + + .current-scale { + font-weight: bold + } + + .desired-scale { + flex: 1.1 0; + } + + .slider-container { + flex: 1 0; + } + + .plus-minus-container { + margin-left: $margin * 2; + .Icon { + --color-active: black; + } + } + + .warning { + color: $colorSoftError; + font-size: small; + display: flex; + align-items: center; + + .Icon { + margin: 0; + margin-right: $margin; + } + } + } +} diff --git a/src/renderer/components/+workloads-replicasets/replicaset-scale-dialog.test.tsx b/src/renderer/components/+workloads-replicasets/replicaset-scale-dialog.test.tsx new file mode 100755 index 0000000000..804b7c344f --- /dev/null +++ b/src/renderer/components/+workloads-replicasets/replicaset-scale-dialog.test.tsx @@ -0,0 +1,167 @@ +import "@testing-library/jest-dom/extend-expect"; + +jest.mock("../../api/endpoints"); +import { ReplicaSetScaleDialog } from "./replicaset-scale-dialog"; +import { render, waitFor, fireEvent } from "@testing-library/react"; +import React from "react"; +import { replicaSetApi } from "../../api/endpoints/replica-set.api"; + +const dummyReplicaSet = { + apiVersion: "v1", + kind: "dummy", + metadata: { + uid: "dummy", + name: "dummy", + creationTimestamp: "dummy", + resourceVersion: "dummy", + selfLink: "link", + }, + selfLink: "link", + spec: { + replicas: 1, + selector: { + matchLabels: { "label": "label" } + }, + template: { + metadata: { + labels: { + app: "label", + }, + }, + spec: { + containers: [{ + name: "dummy", + image: "dummy", + imagePullPolicy: "dummy", + }], + initContainers: [{ + name: "dummy", + image: "dummy", + imagePullPolicy: "dummy", + }], + priority: 1, + serviceAccountName: "dummy", + serviceAccount: "dummy", + securityContext: {}, + schedulerName: "dummy", + }, + }, + minReadySeconds: 1, + }, + status: { + replicas: 1, + fullyLabeledReplicas: 1, + readyReplicas: 1, + availableReplicas: 1, + observedGeneration: 1, + conditions: [{ + type: "dummy", + status: "dummy", + lastUpdateTime: "dummy", + lastTransitionTime: "dummy", + reason: "dummy", + message: "dummy", + }], + }, + getDesired: jest.fn(), + getCurrent: jest.fn(), + getReady: jest.fn(), + getImages: jest.fn(), + getReplicas: jest.fn(), + getSelectors: jest.fn(), + getTemplateLabels: jest.fn(), + getAffinity: jest.fn(), + getTolerations: jest.fn(), + getNodeSelectors: jest.fn(), + getAffinityNumber: jest.fn(), + getId: jest.fn(), + getResourceVersion: jest.fn(), + getName: jest.fn(), + getNs: jest.fn(), + getAge: jest.fn(), + getFinalizers: jest.fn(), + getLabels: jest.fn(), + getAnnotations: jest.fn(), + getOwnerRefs: jest.fn(), + getSearchFields: jest.fn(), + toPlainObject: jest.fn(), + update: jest.fn(), + delete: jest.fn(), +}; + +describe("", () => { + it("renders w/o errors", () => { + const { container } = render(); + + expect(container).toBeInstanceOf(HTMLElement); + }); + + it("init with a dummy replica set and mocked current/desired scale", async () => { + // mock replicaSetApi.getReplicas() which will be called + // when rendered. + const initReplicas = 1; + + replicaSetApi.getReplicas = jest.fn().mockImplementationOnce(async () => initReplicas); + const { getByTestId } = render(); + + ReplicaSetScaleDialog.open(dummyReplicaSet); + // we need to wait for the replicaSetScaleDialog to show up + // because there is an in which renders null at start. + await waitFor(async () => { + const [currentScale, desiredScale] = await Promise.all([ + getByTestId("current-scale"), + getByTestId("desired-scale"), + ]); + + expect(currentScale).toHaveTextContent(`${initReplicas}`); + expect(desiredScale).toHaveTextContent(`${initReplicas}`); + }); + }); + + it("changes the desired scale when clicking the icon buttons +/-", async () => { + const initReplicas = 1; + + replicaSetApi.getReplicas = jest.fn().mockImplementationOnce(async () => initReplicas); + const component = render(); + + ReplicaSetScaleDialog.open(dummyReplicaSet); + await waitFor(async () => { + expect(await component.findByTestId("desired-scale")).toHaveTextContent(`${initReplicas}`); + expect(await component.findByTestId("current-scale")).toHaveTextContent(`${initReplicas}`); + expect((await component.baseElement.querySelector("input").value)).toBe(`${initReplicas}`); + }); + + const up = await component.findByTestId("desired-replicas-up"); + const down = await component.findByTestId("desired-replicas-down"); + + fireEvent.click(up); + expect(await component.findByTestId("desired-scale")).toHaveTextContent(`${initReplicas + 1}`); + expect(await component.findByTestId("current-scale")).toHaveTextContent(`${initReplicas}`); + expect((await component.baseElement.querySelector("input").value)).toBe(`${initReplicas + 1}`); + + fireEvent.click(down); + expect(await component.findByTestId("desired-scale")).toHaveTextContent(`${initReplicas}`); + expect(await component.findByTestId("current-scale")).toHaveTextContent(`${initReplicas}`); + expect((await component.baseElement.querySelector("input").value)).toBe(`${initReplicas}`); + + // edge case, desiredScale must >= 0 + let times = 10; + + for (let i = 0; i < times; i++) { + fireEvent.click(down); + } + expect(await component.findByTestId("desired-scale")).toHaveTextContent("0"); + expect((await component.baseElement.querySelector("input").value)).toBe("0"); + + // edge case, desiredScale must <= scaleMax (100) + times = 120; + + for (let i = 0; i < times; i++) { + fireEvent.click(up); + } + expect(await component.findByTestId("desired-scale")).toHaveTextContent("100"); + expect((component.baseElement.querySelector("input").value)).toBe("100"); + expect(await component.findByTestId("warning")) + .toHaveTextContent("High number of replicas may cause cluster performance issues"); + }); +}); diff --git a/src/renderer/components/+workloads-replicasets/replicaset-scale-dialog.tsx b/src/renderer/components/+workloads-replicasets/replicaset-scale-dialog.tsx new file mode 100644 index 0000000000..0f2a980654 --- /dev/null +++ b/src/renderer/components/+workloads-replicasets/replicaset-scale-dialog.tsx @@ -0,0 +1,169 @@ +import "./replicaset-scale-dialog.scss"; + +import React, { Component } from "react"; +import { computed, observable } from "mobx"; +import { observer } from "mobx-react"; +import { Trans } from "@lingui/macro"; +import { Dialog, DialogProps } from "../dialog"; +import { Wizard, WizardStep } from "../wizard"; +import { Icon } from "../icon"; +import { Slider } from "../slider"; +import { Notifications } from "../notifications"; +import { cssNames } from "../../utils"; +import { ReplicaSet, replicaSetApi } from "../../api/endpoints/replica-set.api"; + +interface Props extends Partial { +} + +@observer +export class ReplicaSetScaleDialog extends Component { + @observable static isOpen = false; + @observable static data: ReplicaSet = null; + + @observable ready = false; + @observable currentReplicas = 0; + @observable desiredReplicas = 0; + + static open(replicaSet: ReplicaSet) { + ReplicaSetScaleDialog.isOpen = true; + ReplicaSetScaleDialog.data = replicaSet; + } + + static close() { + ReplicaSetScaleDialog.isOpen = false; + } + + get replicaSet() { + return ReplicaSetScaleDialog.data; + } + + close = () => { + ReplicaSetScaleDialog.close(); + }; + + onOpen = async () => { + const { replicaSet } = this; + + this.currentReplicas = await replicaSetApi.getReplicas({ + namespace: replicaSet.getNs(), + name: replicaSet.getName(), + }); + this.desiredReplicas = this.currentReplicas; + this.ready = true; + }; + + onClose = () => { + this.ready = false; + }; + + onChange = (evt: React.ChangeEvent, value: number) => { + this.desiredReplicas = value; + }; + + @computed get scaleMax() { + const { currentReplicas } = this; + const defaultMax = 50; + + return currentReplicas <= defaultMax + ? defaultMax * 2 + : currentReplicas * 2; + } + + scale = async () => { + const { replicaSet } = this; + const { currentReplicas, desiredReplicas, close } = this; + + try { + if (currentReplicas !== desiredReplicas) { + await replicaSetApi.scale({ + name: replicaSet.getName(), + namespace: replicaSet.getNs(), + }, desiredReplicas); + } + close(); + } catch (err) { + Notifications.error(err); + } + }; + + desiredReplicasUp = () => { + this.desiredReplicas < this.scaleMax && this.desiredReplicas++; + }; + + desiredReplicasDown = () => { + this.desiredReplicas > 0 && this.desiredReplicas--; + }; + + renderContents() { + const { currentReplicas, desiredReplicas, onChange, scaleMax } = this; + const warning = currentReplicas < 10 && desiredReplicas > 90; + + return ( + <> +
+ Current replica scale: {currentReplicas} +
+
+
+ Desired number of replicas: {desiredReplicas} +
+
+ +
+
+ + +
+
+ {warning && +
+ + High number of replicas may cause cluster performance issues +
+ } + + ); + } + + render() { + const { className, ...dialogProps } = this.props; + const replicaSetName = this.replicaSet ? this.replicaSet.getName() : ""; + const header = ( +
+ Scale Replica Set {replicaSetName} +
+ ); + + return ( + + + Scale} + disabledNext={!this.ready} + > + {this.renderContents()} + + + + ); + } +} diff --git a/src/renderer/components/+workloads-replicasets/replicasets.scss b/src/renderer/components/+workloads-replicasets/replicasets.scss index cda3edf4ee..3d2cbdae17 100644 --- a/src/renderer/components/+workloads-replicasets/replicasets.scss +++ b/src/renderer/components/+workloads-replicasets/replicasets.scss @@ -1,20 +1,5 @@ .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; } @@ -22,13 +7,5 @@ &.warning { @include table-cell-warning; } - - &.namespace { - flex-grow: 1.2; - } - - &.actions { - @include table-cell-action; - } } -} \ No newline at end of file +} diff --git a/src/renderer/components/+workloads-replicasets/replicasets.store.ts b/src/renderer/components/+workloads-replicasets/replicasets.store.ts index 506ff29bd5..337f9c0ae1 100644 --- a/src/renderer/components/+workloads-replicasets/replicasets.store.ts +++ b/src/renderer/components/+workloads-replicasets/replicasets.store.ts @@ -4,6 +4,7 @@ import { KubeObjectStore } from "../../kube-object.store"; import { Deployment, IPodMetrics, podsApi, ReplicaSet, replicaSetApi } from "../../api/endpoints"; import { podsStore } from "../+workloads-pods/pods.store"; import { apiManager } from "../../api/api-manager"; +import { PodStatus } from "../../api/endpoints/pods.api"; @autobind() export class ReplicaSetStore extends KubeObjectStore { @@ -20,6 +21,26 @@ export class ReplicaSetStore extends KubeObjectStore { return podsStore.getPodsByOwner(replicaSet); } + getStatuses(replicaSets: ReplicaSet[]) { + const status = { failed: 0, pending: 0, running: 0 }; + + replicaSets.forEach(replicaSet => { + const pods = this.getChildPods(replicaSet); + + if (pods.some(pod => pod.getStatus() === PodStatus.FAILED)) { + status.failed++; + } + else if (pods.some(pod => pod.getStatus() === PodStatus.PENDING)) { + status.pending++; + } + else { + status.running++; + } + }); + + return status; + } + getReplicaSetsByOwner(deployment: Deployment) { return this.items.filter(replicaSet => !!replicaSet.getOwnerRefs().find(owner => owner.uid === deployment.getId()) diff --git a/src/renderer/components/+workloads-replicasets/replicasets.tsx b/src/renderer/components/+workloads-replicasets/replicasets.tsx index 1febfa7acf..a431aaaa49 100644 --- a/src/renderer/components/+workloads-replicasets/replicasets.tsx +++ b/src/renderer/components/+workloads-replicasets/replicasets.tsx @@ -2,97 +2,93 @@ import "./replicasets.scss"; import React from "react"; import { observer } from "mobx-react"; -import { Trans } from "@lingui/macro"; +import { t, Trans } from "@lingui/macro"; import { ReplicaSet } from "../../api/endpoints"; -import { KubeObjectMenu, KubeObjectMenuProps } from "../kube-object/kube-object-menu"; +import { KubeObjectMenuProps } from "../kube-object/kube-object-menu"; import { replicaSetStore } from "./replicasets.store"; -import { Spinner } from "../spinner"; -import { prevDefault, stopPropagation } from "../../utils"; -import { DrawerTitle } from "../drawer"; -import { Table, TableCell, TableHead, TableRow } from "../table"; -import { showDetails } from "../../navigation"; import { KubeObjectStatusIcon } from "../kube-object-status-icon"; +import { RouteComponentProps } from "react-router"; +import { IReplicaSetsRouteParams } from "../+workloads/workloads.route"; +import { KubeObjectListLayout } from "../kube-object/kube-object-list-layout"; +import { MenuItem } from "../menu/menu"; +import { Icon } from "../icon/icon"; +import { _i18n } from "../../i18n"; +import { kubeObjectMenuRegistry } from "../../../extensions/registries/kube-object-menu-registry"; +import { ReplicaSetScaleDialog } from "./replicaset-scale-dialog"; enum sortBy { name = "name", namespace = "namespace", - pods = "pods", + desired = "desired", + current = "current", + ready = "ready", age = "age", } -interface Props { - replicaSets: ReplicaSet[]; +interface Props extends RouteComponentProps { } @observer export class ReplicaSets 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), - }; - - getPodsLength(replicaSet: ReplicaSet) { - return replicaSetStore.getChildPods(replicaSet).length; - } - render() { - const { replicaSets } = this.props; - - if (!replicaSets.length && !replicaSetStore.isLoaded) return ( -
- ); - if (!replicaSets.length) return null; - return ( -
- Deploy Revisions}/> - - - Name - - Namespace - Pods - Age - - - { - replicaSets.map(replica => { - return ( - showDetails(replica.selfLink, false))} - > - {replica.getName()} - - {replica.getNs()} - {this.getPodsLength(replica)} - {replica.getAge()} - - - - - ); - }) - } -
-
+ replicaSet.getName(), + [sortBy.namespace]: (replicaSet: ReplicaSet) => replicaSet.getNs(), + [sortBy.desired]: (replicaSet: ReplicaSet) => replicaSet.getDesired(), + [sortBy.current]: (replicaSet: ReplicaSet) => replicaSet.getCurrent(), + [sortBy.ready]: (replicaSet: ReplicaSet) => replicaSet.getReady(), + [sortBy.age]: (replicaSet: ReplicaSet) => replicaSet.metadata.creationTimestamp, + }} + searchFilters={[ + (replicaSet: ReplicaSet) => replicaSet.getSearchFields(), + ]} + renderHeaderTitle={Replica Sets} + renderTableHeader={[ + { title: Name, className: "name", sortBy: sortBy.name }, + { className: "warning" }, + { title: Namespace, className: "namespace", sortBy: sortBy.namespace }, + { title: Desired, className: "desired", sortBy: sortBy.desired }, + { title: Current, className: "current", sortBy: sortBy.current }, + { title: Ready, className: "ready", sortBy: sortBy.ready }, + { title: Age, className: "age", sortBy: sortBy.age }, + ]} + renderTableContents={(replicaSet: ReplicaSet) => [ + replicaSet.getName(), + , + replicaSet.getNs(), + replicaSet.getDesired(), + replicaSet.getCurrent(), + replicaSet.getReady(), + replicaSet.getAge(), + ]} + renderItemMenu={(item: ReplicaSet) => { + return ; + }} + /> ); } } export function ReplicaSetMenu(props: KubeObjectMenuProps) { + const { object, toolbar } = props; + return ( - + <> + ReplicaSetScaleDialog.open(object)}> + + Scale + + ); } + +kubeObjectMenuRegistry.add({ + kind: "ReplicaSet", + apiVersions: ["apps/v1"], + components: { + MenuItem: ReplicaSetMenu + } +}); diff --git a/src/renderer/components/+workloads/workloads.route.ts b/src/renderer/components/+workloads/workloads.route.ts index a0062e7336..44c43c5ef9 100644 --- a/src/renderer/components/+workloads/workloads.route.ts +++ b/src/renderer/components/+workloads/workloads.route.ts @@ -25,6 +25,9 @@ export const daemonSetsRoute: RouteProps = { export const statefulSetsRoute: RouteProps = { path: "/statefulsets" }; +export const replicaSetsRoute: RouteProps = { + path: "/replicasets" +}; export const jobsRoute: RouteProps = { path: "/jobs" }; @@ -48,6 +51,9 @@ export interface IDaemonSetsRouteParams { export interface IStatefulSetsRouteParams { } +export interface IReplicaSetsRouteParams { +} + export interface IJobsRouteParams { } @@ -61,6 +67,7 @@ export const podsURL = buildURL(podsRoute.path); export const deploymentsURL = buildURL(deploymentsRoute.path); export const daemonSetsURL = buildURL(daemonSetsRoute.path); export const statefulSetsURL = buildURL(statefulSetsRoute.path); +export const replicaSetsURL = buildURL(replicaSetsRoute.path); export const jobsURL = buildURL(jobsRoute.path); export const cronJobsURL = buildURL(cronJobsRoute.path); @@ -69,6 +76,7 @@ export const workloadURL: Partial> = { "pods": podsStore, "deployments": deploymentStore, "daemonsets": daemonSetStore, "statefulsets": statefulSetStore, + "replicasets": replicaSetStore, "jobs": jobStore, "cronjobs": cronJobStore, }; diff --git a/src/renderer/components/+workloads/workloads.tsx b/src/renderer/components/+workloads/workloads.tsx index d71ddbe177..1cdd6714fe 100644 --- a/src/renderer/components/+workloads/workloads.tsx +++ b/src/renderer/components/+workloads/workloads.tsx @@ -5,7 +5,7 @@ import { observer } from "mobx-react"; import { Trans } from "@lingui/macro"; import { TabLayout, TabLayoutRoute } from "../layout/tab-layout"; import { WorkloadsOverview } from "../+workloads-overview/overview"; -import { cronJobsRoute, cronJobsURL, daemonSetsRoute, daemonSetsURL, deploymentsRoute, deploymentsURL, jobsRoute, jobsURL, overviewRoute, overviewURL, podsRoute, podsURL, statefulSetsRoute, statefulSetsURL } from "./workloads.route"; +import { cronJobsRoute, cronJobsURL, daemonSetsRoute, daemonSetsURL, deploymentsRoute, deploymentsURL, jobsRoute, jobsURL, overviewRoute, overviewURL, podsRoute, podsURL, replicaSetsRoute, replicaSetsURL, statefulSetsRoute, statefulSetsURL } from "./workloads.route"; import { namespaceStore } from "../+namespaces/namespace.store"; import { Pods } from "../+workloads-pods"; import { Deployments } from "../+workloads-deployments"; @@ -14,6 +14,7 @@ import { StatefulSets } from "../+workloads-statefulsets"; import { Jobs } from "../+workloads-jobs"; import { CronJobs } from "../+workloads-cronjobs"; import { isAllowedResource } from "../../../common/rbac"; +import { ReplicaSets } from "../+workloads-replicasets"; @observer export class Workloads extends React.Component { @@ -64,6 +65,15 @@ export class Workloads extends React.Component { }); } + if (isAllowedResource("replicasets")) { + routes.push({ + title: ReplicaSets, + component: ReplicaSets, + url: replicaSetsURL({ query }), + routePath: replicaSetsRoute.path.toString(), + }); + } + if (isAllowedResource("jobs")) { routes.push({ title: Jobs, diff --git a/src/renderer/components/app.tsx b/src/renderer/components/app.tsx index 1fb57fc0b9..1cc5f910b6 100755 --- a/src/renderer/components/app.tsx +++ b/src/renderer/components/app.tsx @@ -48,6 +48,7 @@ import { reaction, computed } from "mobx"; import { nodesStore } from "./+nodes/nodes.store"; import { podsStore } from "./+workloads-pods/pods.store"; import { sum } from "lodash"; +import { ReplicaSetScaleDialog } from "./+workloads-replicasets/replicaset-scale-dialog"; @observer export class App extends React.Component { @@ -204,6 +205,7 @@ export class App extends React.Component { +