1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

Full support for ReplicaSets (#1704)

Signed-off-by: vshakirova <vshakirova@mirantis.com>
This commit is contained in:
Violetta 2020-12-10 16:23:51 +04:00 committed by GitHub
parent d961d8b159
commit d143b234b7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 677 additions and 154 deletions

View File

@ -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",

View File

@ -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}</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}</0>"
msgstr "Scale Deployment <0>{deploymentName}</0>"
#: src/renderer/components/+workloads-replicasets/replicaset-scale-dialog.tsx:143
msgid "Scale Replica Set <0>{replicaSetName}</0>"
msgstr "Scale Replica Set <0>{replicaSetName}</0>"
#: src/renderer/components/+workloads-statefulsets/statefulset-scale-dialog.tsx:139
msgid "Scale Stateful Set <0>{statefulSetName}</0>"
msgstr "Scale Stateful Set <0>{statefulSetName}</0>"

View File

@ -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}</0>"
msgstr ""
#: src/renderer/components/+workloads-replicasets/replicaset-scale-dialog.tsx:143
msgid "Scale Replica Set <0>{replicaSetName}</0>"
msgstr ""
#: src/renderer/components/+workloads-statefulsets/statefulset-scale-dialog.tsx:139
msgid "Scale Stateful Set <0>{statefulSetName}</0>"
msgstr ""

View File

@ -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}</0>"
msgstr "Масштабировать Deployment <0>{deploymentName}</0>"
#: src/renderer/components/+workloads-replicasets/replicaset-scale-dialog.tsx:143
msgid "Scale Replica Set <0>{replicaSetName}</0>"
msgstr "Масштабировать Replica Set <0>{replicaSetName}</0>"
#: src/renderer/components/+workloads-statefulsets/statefulset-scale-dialog.tsx:139
msgid "Scale Stateful Set <0>{statefulSetName}</0>"
msgstr "Масштабировать Stateful Set <0>{statefulSetName}</0>"

View File

@ -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" },

View File

@ -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;
};

View File

@ -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<ReplicaSet> {
protected getScaleApiUrl(params: { namespace: string; name: string }) {
return `${this.getUrl(params)}/scale`;
}
getReplicas(params: { namespace: string; name: string }): Promise<number> {
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,
});

View File

@ -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<Props> {
if (!podsStore.isLoaded) {
podsStore.loadAll();
}
if (!replicaSetStore.isLoaded) {
replicaSetStore.loadAll();
}
}
componentWillUnmount() {
@ -56,7 +50,6 @@ export class DeploymentDetails extends React.Component<Props> {
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<Props> {
<PodDetailsTolerations workload={deployment}/>
<PodDetailsAffinities workload={deployment}/>
<ResourceMetricsText metrics={metrics}/>
<ReplicaSets replicaSets={replicaSets}/>
<PodDetailsList pods={childPods} owner={deployment}/>
</div>
);

View File

@ -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;

View File

@ -18,6 +18,7 @@ const resources: KubeResource[] = [
"deployments",
"statefulsets",
"daemonsets",
"replicasets",
"jobs",
"cronjobs",
];

View File

@ -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;
}
}
}
}

View File

@ -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("<ReplicaSetScaleDialog />", () => {
it("renders w/o errors", () => {
const { container } = render(<ReplicaSetScaleDialog/>);
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 <ReplicaSetScaleDialog /> rendered.
const initReplicas = 1;
replicaSetApi.getReplicas = jest.fn().mockImplementationOnce(async () => initReplicas);
const { getByTestId } = render(<ReplicaSetScaleDialog/>);
ReplicaSetScaleDialog.open(dummyReplicaSet);
// we need to wait for the replicaSetScaleDialog to show up
// because there is an <Animate /> in <Dialog /> 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/>);
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");
});
});

View File

@ -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<DialogProps> {
}
@observer
export class ReplicaSetScaleDialog extends Component<Props> {
@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 (
<>
<div className="current-scale" data-testid="current-scale">
<Trans>Current replica scale: {currentReplicas}</Trans>
</div>
<div className="flex gaps align-center">
<div className="desired-scale" data-testid="desired-scale">
<Trans>Desired number of replicas</Trans>: {desiredReplicas}
</div>
<div className="slider-container flex align-center" data-testid="slider">
<Slider value={desiredReplicas} max={scaleMax}
onChange={onChange as any /** see: https://github.com/mui-org/material-ui/issues/20191 */}
/>
</div>
<div className="plus-minus-container flex gaps">
<Icon
material="add_circle_outline"
onClick={this.desiredReplicasUp}
data-testid="desired-replicas-up"
/>
<Icon
material="remove_circle_outline"
onClick={this.desiredReplicasDown}
data-testid="desired-replicas-down"
/>
</div>
</div>
{warning &&
<div className="warning" data-testid="warning">
<Icon material="warning"/>
<Trans>High number of replicas may cause cluster performance issues</Trans>
</div>
}
</>
);
}
render() {
const { className, ...dialogProps } = this.props;
const replicaSetName = this.replicaSet ? this.replicaSet.getName() : "";
const header = (
<h5>
<Trans>Scale Replica Set <span>{replicaSetName}</span></Trans>
</h5>
);
return (
<Dialog
{...dialogProps}
isOpen={ReplicaSetScaleDialog.isOpen}
className={cssNames("ReplicaSetScaleDialog", className)}
onOpen={this.onOpen}
onClose={this.onClose}
close={this.close}
>
<Wizard header={header} done={this.close}>
<WizardStep
contentClass="flex gaps column"
next={this.scale}
nextLabel={<Trans>Scale</Trans>}
disabledNext={!this.ready}
>
{this.renderContents()}
</WizardStep>
</Wizard>
</Dialog>
);
}
}

View File

@ -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;
}
}
}

View File

@ -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<ReplicaSet> {
@ -20,6 +21,26 @@ export class ReplicaSetStore extends KubeObjectStore<ReplicaSet> {
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())

View File

@ -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<IReplicaSetsRouteParams> {
}
@observer
export class ReplicaSets extends React.Component<Props> {
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 (
<div className="ReplicaSets"><Spinner center/></div>
);
if (!replicaSets.length) return null;
return (
<div className="ReplicaSets flex column">
<DrawerTitle title={<Trans>Deploy Revisions</Trans>}/>
<Table
selectable
scrollable={false}
sortable={this.sortingCallbacks}
sortByDefault={{ sortBy: sortBy.pods, orderBy: "desc" }}
sortSyncWithUrl={false}
className="box grow"
>
<TableHead>
<TableCell className="name" sortBy={sortBy.name}><Trans>Name</Trans></TableCell>
<TableCell className="warning"/>
<TableCell className="namespace" sortBy={sortBy.namespace}>Namespace</TableCell>
<TableCell className="pods" sortBy={sortBy.pods}><Trans>Pods</Trans></TableCell>
<TableCell className="age" sortBy={sortBy.age}><Trans>Age</Trans></TableCell>
<TableCell className="actions"/>
</TableHead>
{
replicaSets.map(replica => {
return (
<TableRow
key={replica.getId()}
sortItem={replica}
nowrap
onClick={prevDefault(() => showDetails(replica.selfLink, false))}
>
<TableCell className="name">{replica.getName()}</TableCell>
<TableCell className="warning"><KubeObjectStatusIcon key="icon" object={replica}/></TableCell>
<TableCell className="namespace">{replica.getNs()}</TableCell>
<TableCell className="pods">{this.getPodsLength(replica)}</TableCell>
<TableCell className="age">{replica.getAge()}</TableCell>
<TableCell className="actions" onClick={stopPropagation}>
<ReplicaSetMenu object={replica}/>
</TableCell>
</TableRow>
);
})
}
</Table>
</div>
<KubeObjectListLayout
className="ReplicaSets" store={replicaSetStore}
sortingCallbacks={{
[sortBy.name]: (replicaSet: ReplicaSet) => 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={<Trans>Replica Sets</Trans>}
renderTableHeader={[
{ title: <Trans>Name</Trans>, className: "name", sortBy: sortBy.name },
{ className: "warning" },
{ title: <Trans>Namespace</Trans>, className: "namespace", sortBy: sortBy.namespace },
{ title: <Trans>Desired</Trans>, className: "desired", sortBy: sortBy.desired },
{ title: <Trans>Current</Trans>, className: "current", sortBy: sortBy.current },
{ title: <Trans>Ready</Trans>, className: "ready", sortBy: sortBy.ready },
{ title: <Trans>Age</Trans>, className: "age", sortBy: sortBy.age },
]}
renderTableContents={(replicaSet: ReplicaSet) => [
replicaSet.getName(),
<KubeObjectStatusIcon key="icon" object={replicaSet}/>,
replicaSet.getNs(),
replicaSet.getDesired(),
replicaSet.getCurrent(),
replicaSet.getReady(),
replicaSet.getAge(),
]}
renderItemMenu={(item: ReplicaSet) => {
return <ReplicaSetMenu object={item}/>;
}}
/>
);
}
}
export function ReplicaSetMenu(props: KubeObjectMenuProps<ReplicaSet>) {
const { object, toolbar } = props;
return (
<KubeObjectMenu {...props}/>
<>
<MenuItem onClick={() => ReplicaSetScaleDialog.open(object)}>
<Icon material="open_with" title={_i18n._(t`Scale`)} interactive={toolbar}/>
<span className="title"><Trans>Scale</Trans></span>
</MenuItem>
</>
);
}
kubeObjectMenuRegistry.add({
kind: "ReplicaSet",
apiVersions: ["apps/v1"],
components: {
MenuItem: ReplicaSetMenu
}
});

View File

@ -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<IPodsRouteParams>(podsRoute.path);
export const deploymentsURL = buildURL<IDeploymentsRouteParams>(deploymentsRoute.path);
export const daemonSetsURL = buildURL<IDaemonSetsRouteParams>(daemonSetsRoute.path);
export const statefulSetsURL = buildURL<IStatefulSetsRouteParams>(statefulSetsRoute.path);
export const replicaSetsURL = buildURL<IReplicaSetsRouteParams>(replicaSetsRoute.path);
export const jobsURL = buildURL<IJobsRouteParams>(jobsRoute.path);
export const cronJobsURL = buildURL<ICronJobsRouteParams>(cronJobsRoute.path);
@ -69,6 +76,7 @@ export const workloadURL: Partial<Record<KubeResource, ReturnType<typeof buildUR
"deployments": deploymentsURL,
"daemonsets": daemonSetsURL,
"statefulsets": statefulSetsURL,
"replicasets": replicaSetsURL,
"jobs": jobsURL,
"cronjobs": cronJobsURL,
};

View File

@ -6,12 +6,14 @@ import { statefulSetStore } from "../+workloads-statefulsets/statefulset.store";
import { jobStore } from "../+workloads-jobs/job.store";
import { cronJobStore } from "../+workloads-cronjobs/cronjob.store";
import { KubeResource } from "../../../common/rbac";
import { replicaSetStore } from "../+workloads-replicasets/replicasets.store";
export const workloadStores: Partial<Record<KubeResource, KubeObjectStore>> = {
"pods": podsStore,
"deployments": deploymentStore,
"daemonsets": daemonSetStore,
"statefulsets": statefulSetStore,
"replicasets": replicaSetStore,
"jobs": jobStore,
"cronjobs": cronJobStore,
};

View File

@ -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: <Trans>ReplicaSets</Trans>,
component: ReplicaSets,
url: replicaSetsURL({ query }),
routePath: replicaSetsRoute.path.toString(),
});
}
if (isAllowedResource("jobs")) {
routes.push({
title: <Trans>Jobs</Trans>,

View File

@ -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 {
<AddRoleBindingDialog/>
<DeploymentScaleDialog/>
<StatefulSetScaleDialog/>
<ReplicaSetScaleDialog/>
<CronJobTriggerDialog/>
</ErrorBoundary>
</Router>