Skip to content

Commit f636580

Browse files
authored
cks,ui: allow changing stopped cluster offering, improvements (#7475)
* cks,ui: allow changing stopped cluster offering, improvements Fixes #7454 - Allows changing compute offering for a stopped cluster - Allows compute offering change when the cluster has autoscaling enabled Signed-off-by: Abhishek Kumar <[email protected]>
1 parent c353588 commit f636580

File tree

6 files changed

+257
-80
lines changed

6 files changed

+257
-80
lines changed

plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/KubernetesClusterManagerImpl.java

Lines changed: 33 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -803,6 +803,34 @@ protected void addKubernetesClusterDetailIfIsNotEmpty(List<KubernetesClusterDeta
803803
}
804804
}
805805

806+
protected void validateKubernetesClusterScaleSize(final KubernetesClusterVO kubernetesCluster, final Long clusterSize, final int maxClusterSize, final DataCenter zone) {
807+
if (clusterSize == null) {
808+
return;
809+
}
810+
if (clusterSize == kubernetesCluster.getNodeCount()) {
811+
return;
812+
}
813+
if (kubernetesCluster.getState().equals(KubernetesCluster.State.Stopped)) { // Cannot scale stopped cluster currently for cluster size
814+
throw new PermissionDeniedException(String.format("Kubernetes cluster : %s is in %s state", kubernetesCluster.getName(), kubernetesCluster.getState().toString()));
815+
}
816+
if (clusterSize < 1) {
817+
throw new InvalidParameterValueException(String.format("Kubernetes cluster : %s cannot be scaled for size, %d", kubernetesCluster.getName(), clusterSize));
818+
}
819+
if (clusterSize + kubernetesCluster.getControlNodeCount() > maxClusterSize) {
820+
throw new InvalidParameterValueException(
821+
String.format("Maximum cluster size can not exceed %d. Please contact your administrator", maxClusterSize));
822+
}
823+
if (clusterSize > kubernetesCluster.getNodeCount()) { // Upscale
824+
VMTemplateVO template = templateDao.findById(kubernetesCluster.getTemplateId());
825+
if (template == null) {
826+
throw new InvalidParameterValueException(String.format("Invalid template associated with Kubernetes cluster : %s", kubernetesCluster.getName()));
827+
}
828+
if (CollectionUtils.isEmpty(templateJoinDao.newTemplateView(template, zone.getId(), true))) {
829+
throw new InvalidParameterValueException(String.format("Template : %s associated with Kubernetes cluster : %s is not in Ready state for datacenter : %s", template.getName(), kubernetesCluster.getName(), zone.getName()));
830+
}
831+
}
832+
}
833+
806834
private void validateKubernetesClusterScaleParameters(ScaleKubernetesClusterCmd cmd) {
807835
final Long kubernetesClusterId = cmd.getId();
808836
final Long serviceOfferingId = cmd.getServiceOfferingId();
@@ -844,8 +872,8 @@ private void validateKubernetesClusterScaleParameters(ScaleKubernetesClusterCmd
844872

845873
int maxClusterSize = KubernetesMaxClusterSize.valueIn(kubernetesCluster.getAccountId());
846874
if (isAutoscalingEnabled != null && isAutoscalingEnabled) {
847-
if (clusterSize != null || serviceOfferingId != null || nodeIds != null) {
848-
throw new InvalidParameterValueException("Autoscaling can not be passed along with nodeids or clustersize or service offering");
875+
if (clusterSize != null || nodeIds != null) {
876+
throw new InvalidParameterValueException("Autoscaling can not be passed along with nodeids or clustersize");
849877
}
850878

851879
if (!KubernetesVersionManagerImpl.versionSupportsAutoscaling(clusterVersion)) {
@@ -914,34 +942,14 @@ private void validateKubernetesClusterScaleParameters(ScaleKubernetesClusterCmd
914942
}
915943
}
916944
final ServiceOffering existingServiceOffering = serviceOfferingDao.findById(kubernetesCluster.getServiceOfferingId());
917-
if (serviceOffering.getRamSize() < existingServiceOffering.getRamSize() ||
918-
serviceOffering.getCpu() * serviceOffering.getSpeed() < existingServiceOffering.getCpu() * existingServiceOffering.getSpeed()) {
945+
if (KubernetesCluster.State.Running.equals(kubernetesCluster.getState()) && (serviceOffering.getRamSize() < existingServiceOffering.getRamSize() ||
946+
serviceOffering.getCpu() * serviceOffering.getSpeed() < existingServiceOffering.getCpu() * existingServiceOffering.getSpeed())) {
919947
logAndThrow(Level.WARN, String.format("Kubernetes cluster cannot be scaled down for service offering. Service offering : %s offers lesser resources as compared to service offering : %s of Kubernetes cluster : %s",
920948
serviceOffering.getName(), existingServiceOffering.getName(), kubernetesCluster.getName()));
921949
}
922950
}
923951

924-
if (clusterSize != null) {
925-
if (kubernetesCluster.getState().equals(KubernetesCluster.State.Stopped)) { // Cannot scale stopped cluster currently for cluster size
926-
throw new PermissionDeniedException(String.format("Kubernetes cluster : %s is in %s state", kubernetesCluster.getName(), kubernetesCluster.getState().toString()));
927-
}
928-
if (clusterSize < 1) {
929-
throw new InvalidParameterValueException(String.format("Kubernetes cluster : %s cannot be scaled for size, %d", kubernetesCluster.getName(), clusterSize));
930-
}
931-
if (clusterSize + kubernetesCluster.getControlNodeCount() > maxClusterSize) {
932-
throw new InvalidParameterValueException(
933-
String.format("Maximum cluster size can not exceed %d. Please contact your administrator", maxClusterSize));
934-
}
935-
if (clusterSize > kubernetesCluster.getNodeCount()) { // Upscale
936-
VMTemplateVO template = templateDao.findById(kubernetesCluster.getTemplateId());
937-
if (template == null) {
938-
throw new InvalidParameterValueException(String.format("Invalid template associated with Kubernetes cluster : %s", kubernetesCluster.getName()));
939-
}
940-
if (CollectionUtils.isEmpty(templateJoinDao.newTemplateView(template, zone.getId(), true))) {
941-
throw new InvalidParameterValueException(String.format("Template : %s associated with Kubernetes cluster : %s is not in Ready state for datacenter : %s", template.getName(), kubernetesCluster.getName(), zone.getName()));
942-
}
943-
}
944-
}
952+
validateKubernetesClusterScaleSize(kubernetesCluster, clusterSize, maxClusterSize, zone);
945953
}
946954

947955
private void validateKubernetesClusterUpgradeParameters(UpgradeKubernetesClusterCmd cmd) {

plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterResourceModifierActionWorker.java

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,29 @@
1717

1818
package com.cloud.kubernetes.cluster.actionworkers;
1919

20+
import static com.cloud.utils.NumbersUtil.toHumanReadableSize;
21+
22+
import java.io.File;
23+
import java.io.IOException;
24+
import java.lang.reflect.Field;
25+
import java.util.ArrayList;
26+
import java.util.HashMap;
27+
import java.util.List;
28+
import java.util.Map;
29+
import java.util.concurrent.ConcurrentHashMap;
30+
31+
import javax.inject.Inject;
32+
33+
import org.apache.cloudstack.api.ApiConstants;
34+
import org.apache.cloudstack.api.BaseCmd;
35+
import org.apache.cloudstack.api.command.user.firewall.CreateFirewallRuleCmd;
36+
import org.apache.cloudstack.api.command.user.vm.StartVMCmd;
37+
import org.apache.cloudstack.api.command.user.volume.ResizeVolumeCmd;
38+
import org.apache.commons.codec.binary.Base64;
39+
import org.apache.commons.collections.CollectionUtils;
40+
import org.apache.commons.lang3.StringUtils;
41+
import org.apache.log4j.Level;
42+
2043
import com.cloud.capacity.CapacityManager;
2144
import com.cloud.dc.ClusterDetailsDao;
2245
import com.cloud.dc.ClusterDetailsVO;
@@ -77,28 +100,6 @@
77100
import com.cloud.vm.VmDetailConstants;
78101
import com.cloud.vm.dao.VMInstanceDao;
79102

80-
import org.apache.cloudstack.api.ApiConstants;
81-
import org.apache.cloudstack.api.BaseCmd;
82-
import org.apache.cloudstack.api.command.user.firewall.CreateFirewallRuleCmd;
83-
import org.apache.cloudstack.api.command.user.vm.StartVMCmd;
84-
import org.apache.cloudstack.api.command.user.volume.ResizeVolumeCmd;
85-
import org.apache.commons.codec.binary.Base64;
86-
import org.apache.commons.collections.CollectionUtils;
87-
import org.apache.commons.lang3.StringUtils;
88-
import org.apache.log4j.Level;
89-
90-
import javax.inject.Inject;
91-
import java.io.File;
92-
import java.io.IOException;
93-
import java.lang.reflect.Field;
94-
import java.util.ArrayList;
95-
import java.util.HashMap;
96-
import java.util.List;
97-
import java.util.Map;
98-
import java.util.concurrent.ConcurrentHashMap;
99-
100-
import static com.cloud.utils.NumbersUtil.toHumanReadableSize;
101-
102103
public class KubernetesClusterResourceModifierActionWorker extends KubernetesClusterActionWorker {
103104

104105
@Inject
@@ -669,7 +670,6 @@ protected boolean autoscaleCluster(boolean enable, Long minSize, Long maxSize) {
669670
} finally {
670671
// Deploying the autoscaler might fail but it can be deployed manually too, so no need to go to an alert state
671672
updateLoginUserDetails(null);
672-
stateTransitTo(kubernetesCluster.getId(), KubernetesCluster.Event.OperationSucceeded);
673673
}
674674
}
675675
}

plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterScaleWorker.java

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828

2929
import org.apache.cloudstack.api.InternalIdentity;
3030
import org.apache.commons.collections.CollectionUtils;
31+
import org.apache.commons.lang3.StringUtils;
3132
import org.apache.log4j.Level;
3233

3334
import com.cloud.dc.DataCenter;
@@ -57,7 +58,6 @@
5758
import com.cloud.vm.VMInstanceVO;
5859
import com.cloud.vm.VirtualMachine;
5960
import com.cloud.vm.dao.VMInstanceDao;
60-
import org.apache.commons.lang3.StringUtils;
6161

6262
public class KubernetesClusterScaleWorker extends KubernetesClusterResourceModifierActionWorker {
6363

@@ -406,6 +406,19 @@ private void scaleKubernetesClusterSize() throws CloudRuntimeException {
406406
kubernetesCluster = updateKubernetesClusterEntry(clusterSize, null);
407407
}
408408

409+
private boolean isAutoscalingChanged() {
410+
if (this.isAutoscalingEnabled == null) {
411+
return false;
412+
}
413+
if (this.isAutoscalingEnabled != kubernetesCluster.getAutoscalingEnabled()) {
414+
return true;
415+
}
416+
if (minSize != null && (!minSize.equals(kubernetesCluster.getMinSize()))) {
417+
return true;
418+
}
419+
return maxSize != null && (!maxSize.equals(kubernetesCluster.getMaxSize()));
420+
}
421+
409422
public boolean scaleCluster() throws CloudRuntimeException {
410423
init();
411424
if (LOGGER.isInfoEnabled()) {
@@ -417,11 +430,17 @@ public boolean scaleCluster() throws CloudRuntimeException {
417430
if (existingServiceOffering == null) {
418431
logAndThrow(Level.ERROR, String.format("Scaling Kubernetes cluster : %s failed, service offering for the Kubernetes cluster not found!", kubernetesCluster.getName()));
419432
}
433+
final boolean autscalingChanged = isAutoscalingChanged();
434+
final boolean serviceOfferingScalingNeeded = serviceOffering != null && serviceOffering.getId() != existingServiceOffering.getId();
420435

421-
if (this.isAutoscalingEnabled != null) {
422-
return autoscaleCluster(this.isAutoscalingEnabled, minSize, maxSize);
436+
if (autscalingChanged) {
437+
boolean autoScaled = autoscaleCluster(this.isAutoscalingEnabled, minSize, maxSize);
438+
if (autoScaled && serviceOfferingScalingNeeded) {
439+
scaleKubernetesClusterOffering();
440+
}
441+
stateTransitTo(kubernetesCluster.getId(), KubernetesCluster.Event.OperationSucceeded);
442+
return autoScaled;
423443
}
424-
final boolean serviceOfferingScalingNeeded = serviceOffering != null && serviceOffering.getId() != existingServiceOffering.getId();
425444
final boolean clusterSizeScalingNeeded = clusterSize != null && clusterSize != originalClusterSize;
426445
final long newVMRequired = clusterSize == null ? 0 : clusterSize - originalClusterSize;
427446
if (serviceOfferingScalingNeeded && clusterSizeScalingNeeded) {
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
package com.cloud.kubernetes.cluster;
18+
19+
import java.util.List;
20+
21+
import org.junit.Test;
22+
import org.junit.runner.RunWith;
23+
import org.mockito.InjectMocks;
24+
import org.mockito.Mock;
25+
import org.mockito.Mockito;
26+
import org.mockito.Spy;
27+
import org.mockito.junit.MockitoJUnitRunner;
28+
29+
import com.cloud.api.query.dao.TemplateJoinDao;
30+
import com.cloud.api.query.vo.TemplateJoinVO;
31+
import com.cloud.dc.DataCenter;
32+
import com.cloud.exception.InvalidParameterValueException;
33+
import com.cloud.exception.PermissionDeniedException;
34+
import com.cloud.storage.VMTemplateVO;
35+
import com.cloud.storage.dao.VMTemplateDao;
36+
37+
@RunWith(MockitoJUnitRunner.class)
38+
public class KubernetesClusterManagerImplTest {
39+
40+
@Mock
41+
VMTemplateDao templateDao;
42+
43+
@Mock
44+
TemplateJoinDao templateJoinDao;
45+
46+
@Spy
47+
@InjectMocks
48+
KubernetesClusterManagerImpl clusterManager;
49+
50+
@Test
51+
public void testValidateKubernetesClusterScaleSizeNullNewSizeNoError() {
52+
clusterManager.validateKubernetesClusterScaleSize(Mockito.mock(KubernetesClusterVO.class), null, 100, Mockito.mock(DataCenter.class));
53+
}
54+
55+
@Test
56+
public void testValidateKubernetesClusterScaleSizeSameNewSizeNoError() {
57+
Long size = 2L;
58+
KubernetesClusterVO clusterVO = Mockito.mock(KubernetesClusterVO.class);
59+
Mockito.when(clusterVO.getNodeCount()).thenReturn(size);
60+
clusterManager.validateKubernetesClusterScaleSize(clusterVO, size, 100, Mockito.mock(DataCenter.class));
61+
}
62+
63+
@Test(expected = PermissionDeniedException.class)
64+
public void testValidateKubernetesClusterScaleSizeStoppedCluster() {
65+
Long size = 2L;
66+
KubernetesClusterVO clusterVO = Mockito.mock(KubernetesClusterVO.class);
67+
Mockito.when(clusterVO.getNodeCount()).thenReturn(size);
68+
Mockito.when(clusterVO.getState()).thenReturn(KubernetesCluster.State.Stopped);
69+
clusterManager.validateKubernetesClusterScaleSize(clusterVO, 3L, 100, Mockito.mock(DataCenter.class));
70+
}
71+
72+
@Test(expected = InvalidParameterValueException.class)
73+
public void testValidateKubernetesClusterScaleSizeZeroNewSize() {
74+
Long size = 2L;
75+
KubernetesClusterVO clusterVO = Mockito.mock(KubernetesClusterVO.class);
76+
Mockito.when(clusterVO.getState()).thenReturn(KubernetesCluster.State.Running);
77+
Mockito.when(clusterVO.getNodeCount()).thenReturn(size);
78+
clusterManager.validateKubernetesClusterScaleSize(clusterVO, 0L, 100, Mockito.mock(DataCenter.class));
79+
}
80+
81+
@Test(expected = InvalidParameterValueException.class)
82+
public void testValidateKubernetesClusterScaleSizeOverMaxSize() {
83+
KubernetesClusterVO clusterVO = Mockito.mock(KubernetesClusterVO.class);
84+
Mockito.when(clusterVO.getState()).thenReturn(KubernetesCluster.State.Running);
85+
Mockito.when(clusterVO.getControlNodeCount()).thenReturn(1L);
86+
clusterManager.validateKubernetesClusterScaleSize(clusterVO, 4L, 4, Mockito.mock(DataCenter.class));
87+
}
88+
89+
@Test
90+
public void testValidateKubernetesClusterScaleSizeDownsacaleNoError() {
91+
KubernetesClusterVO clusterVO = Mockito.mock(KubernetesClusterVO.class);
92+
Mockito.when(clusterVO.getState()).thenReturn(KubernetesCluster.State.Running);
93+
Mockito.when(clusterVO.getControlNodeCount()).thenReturn(1L);
94+
Mockito.when(clusterVO.getNodeCount()).thenReturn(4L);
95+
clusterManager.validateKubernetesClusterScaleSize(clusterVO, 2L, 10, Mockito.mock(DataCenter.class));
96+
}
97+
98+
@Test(expected = InvalidParameterValueException.class)
99+
public void testValidateKubernetesClusterScaleSizeUpscaleDeletedTemplate() {
100+
KubernetesClusterVO clusterVO = Mockito.mock(KubernetesClusterVO.class);
101+
Mockito.when(clusterVO.getState()).thenReturn(KubernetesCluster.State.Running);
102+
Mockito.when(clusterVO.getControlNodeCount()).thenReturn(1L);
103+
Mockito.when(clusterVO.getNodeCount()).thenReturn(2L);
104+
Mockito.when(templateDao.findById(Mockito.anyLong())).thenReturn(null);
105+
clusterManager.validateKubernetesClusterScaleSize(clusterVO, 4L, 10, Mockito.mock(DataCenter.class));
106+
}
107+
108+
@Test(expected = InvalidParameterValueException.class)
109+
public void testValidateKubernetesClusterScaleSizeUpscaleNotInZoneTemplate() {
110+
KubernetesClusterVO clusterVO = Mockito.mock(KubernetesClusterVO.class);
111+
Mockito.when(clusterVO.getState()).thenReturn(KubernetesCluster.State.Running);
112+
Mockito.when(clusterVO.getControlNodeCount()).thenReturn(1L);
113+
Mockito.when(clusterVO.getNodeCount()).thenReturn(2L);
114+
Mockito.when(templateDao.findById(Mockito.anyLong())).thenReturn(Mockito.mock(VMTemplateVO.class));
115+
Mockito.when(templateJoinDao.newTemplateView(Mockito.any(VMTemplateVO.class), Mockito.anyLong(), Mockito.anyBoolean())).thenReturn(null);
116+
clusterManager.validateKubernetesClusterScaleSize(clusterVO, 4L, 10, Mockito.mock(DataCenter.class));
117+
}
118+
119+
@Test
120+
public void testValidateKubernetesClusterScaleSizeUpscaleNoError() {
121+
KubernetesClusterVO clusterVO = Mockito.mock(KubernetesClusterVO.class);
122+
Mockito.when(clusterVO.getState()).thenReturn(KubernetesCluster.State.Running);
123+
Mockito.when(clusterVO.getControlNodeCount()).thenReturn(1L);
124+
Mockito.when(clusterVO.getNodeCount()).thenReturn(2L);
125+
Mockito.when(templateDao.findById(Mockito.anyLong())).thenReturn(Mockito.mock(VMTemplateVO.class));
126+
Mockito.when(templateJoinDao.newTemplateView(Mockito.any(VMTemplateVO.class), Mockito.anyLong(), Mockito.anyBoolean())).thenReturn(List.of(Mockito.mock(TemplateJoinVO.class)));
127+
clusterManager.validateKubernetesClusterScaleSize(clusterVO, 4L, 10, Mockito.mock(DataCenter.class));
128+
}
129+
}

ui/src/config/section/compute.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -504,7 +504,7 @@ export default {
504504
message: 'message.kubernetes.cluster.scale',
505505
docHelp: 'plugins/cloudstack-kubernetes-service.html#scaling-kubernetes-cluster',
506506
dataView: true,
507-
show: (record) => { return ['Created', 'Running'].includes(record.state) },
507+
show: (record) => { return ['Created', 'Running', 'Stopped'].includes(record.state) },
508508
popup: true,
509509
component: shallowRef(defineAsyncComponent(() => import('@/views/compute/ScaleKubernetesCluster.vue')))
510510
},

0 commit comments

Comments
 (0)