From c3535880d2588782b12bb61523a53549e6c0d778 Mon Sep 17 00:00:00 2001 From: Ben <5091256+benj-n@users.noreply.github.com> Date: Fri, 26 May 2023 06:44:02 -0400 Subject: [PATCH 1/6] Create user 'cloud' in cloudstack-usage postinstall (#7559) This ensures the chown 'cloud:cloud' command (later in the same file) is always performed with no error. --- debian/cloudstack-usage.postinst | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/debian/cloudstack-usage.postinst b/debian/cloudstack-usage.postinst index 61a6b14ce32..8e262254f12 100755 --- a/debian/cloudstack-usage.postinst +++ b/debian/cloudstack-usage.postinst @@ -21,7 +21,12 @@ set -e case "$1" in configure) - + if ! getent passwd cloud >/dev/null; then + adduser --quiet --system --group --no-create-home --home /var/lib/cloudstack/management cloud + else + usermod -m -d /var/lib/cloudstack/management cloud || true + fi + # Linking usage server db.properties to management server db.properties if [ -f "/etc/cloudstack/management/db.properties" ]; then echo "Replacing usage server's db.properties with a link to the management server's db.properties" From f636580195338fdecdd8edb85a7cc7313e8f0251 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Mon, 29 May 2023 15:58:06 +0530 Subject: [PATCH 2/6] 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 --- .../cluster/KubernetesClusterManagerImpl.java | 58 ++++---- ...esClusterResourceModifierActionWorker.java | 46 +++---- .../KubernetesClusterScaleWorker.java | 29 +++- .../KubernetesClusterManagerImplTest.java | 129 ++++++++++++++++++ ui/src/config/section/compute.js | 2 +- .../views/compute/ScaleKubernetesCluster.vue | 75 ++++++---- 6 files changed, 258 insertions(+), 81 deletions(-) create mode 100644 plugins/integrations/kubernetes-service/src/test/java/com/cloud/kubernetes/cluster/KubernetesClusterManagerImplTest.java diff --git a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/KubernetesClusterManagerImpl.java b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/KubernetesClusterManagerImpl.java index 150d203dd41..e3662161fba 100644 --- a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/KubernetesClusterManagerImpl.java +++ b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/KubernetesClusterManagerImpl.java @@ -803,6 +803,34 @@ public class KubernetesClusterManagerImpl extends ManagerBase implements Kuberne } } + protected void validateKubernetesClusterScaleSize(final KubernetesClusterVO kubernetesCluster, final Long clusterSize, final int maxClusterSize, final DataCenter zone) { + if (clusterSize == null) { + return; + } + if (clusterSize == kubernetesCluster.getNodeCount()) { + return; + } + if (kubernetesCluster.getState().equals(KubernetesCluster.State.Stopped)) { // Cannot scale stopped cluster currently for cluster size + throw new PermissionDeniedException(String.format("Kubernetes cluster : %s is in %s state", kubernetesCluster.getName(), kubernetesCluster.getState().toString())); + } + if (clusterSize < 1) { + throw new InvalidParameterValueException(String.format("Kubernetes cluster : %s cannot be scaled for size, %d", kubernetesCluster.getName(), clusterSize)); + } + if (clusterSize + kubernetesCluster.getControlNodeCount() > maxClusterSize) { + throw new InvalidParameterValueException( + String.format("Maximum cluster size can not exceed %d. Please contact your administrator", maxClusterSize)); + } + if (clusterSize > kubernetesCluster.getNodeCount()) { // Upscale + VMTemplateVO template = templateDao.findById(kubernetesCluster.getTemplateId()); + if (template == null) { + throw new InvalidParameterValueException(String.format("Invalid template associated with Kubernetes cluster : %s", kubernetesCluster.getName())); + } + if (CollectionUtils.isEmpty(templateJoinDao.newTemplateView(template, zone.getId(), true))) { + 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())); + } + } + } + private void validateKubernetesClusterScaleParameters(ScaleKubernetesClusterCmd cmd) { final Long kubernetesClusterId = cmd.getId(); final Long serviceOfferingId = cmd.getServiceOfferingId(); @@ -844,8 +872,8 @@ public class KubernetesClusterManagerImpl extends ManagerBase implements Kuberne int maxClusterSize = KubernetesMaxClusterSize.valueIn(kubernetesCluster.getAccountId()); if (isAutoscalingEnabled != null && isAutoscalingEnabled) { - if (clusterSize != null || serviceOfferingId != null || nodeIds != null) { - throw new InvalidParameterValueException("Autoscaling can not be passed along with nodeids or clustersize or service offering"); + if (clusterSize != null || nodeIds != null) { + throw new InvalidParameterValueException("Autoscaling can not be passed along with nodeids or clustersize"); } if (!KubernetesVersionManagerImpl.versionSupportsAutoscaling(clusterVersion)) { @@ -914,34 +942,14 @@ public class KubernetesClusterManagerImpl extends ManagerBase implements Kuberne } } final ServiceOffering existingServiceOffering = serviceOfferingDao.findById(kubernetesCluster.getServiceOfferingId()); - if (serviceOffering.getRamSize() < existingServiceOffering.getRamSize() || - serviceOffering.getCpu() * serviceOffering.getSpeed() < existingServiceOffering.getCpu() * existingServiceOffering.getSpeed()) { + if (KubernetesCluster.State.Running.equals(kubernetesCluster.getState()) && (serviceOffering.getRamSize() < existingServiceOffering.getRamSize() || + serviceOffering.getCpu() * serviceOffering.getSpeed() < existingServiceOffering.getCpu() * existingServiceOffering.getSpeed())) { 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", serviceOffering.getName(), existingServiceOffering.getName(), kubernetesCluster.getName())); } } - if (clusterSize != null) { - if (kubernetesCluster.getState().equals(KubernetesCluster.State.Stopped)) { // Cannot scale stopped cluster currently for cluster size - throw new PermissionDeniedException(String.format("Kubernetes cluster : %s is in %s state", kubernetesCluster.getName(), kubernetesCluster.getState().toString())); - } - if (clusterSize < 1) { - throw new InvalidParameterValueException(String.format("Kubernetes cluster : %s cannot be scaled for size, %d", kubernetesCluster.getName(), clusterSize)); - } - if (clusterSize + kubernetesCluster.getControlNodeCount() > maxClusterSize) { - throw new InvalidParameterValueException( - String.format("Maximum cluster size can not exceed %d. Please contact your administrator", maxClusterSize)); - } - if (clusterSize > kubernetesCluster.getNodeCount()) { // Upscale - VMTemplateVO template = templateDao.findById(kubernetesCluster.getTemplateId()); - if (template == null) { - throw new InvalidParameterValueException(String.format("Invalid template associated with Kubernetes cluster : %s", kubernetesCluster.getName())); - } - if (CollectionUtils.isEmpty(templateJoinDao.newTemplateView(template, zone.getId(), true))) { - 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())); - } - } - } + validateKubernetesClusterScaleSize(kubernetesCluster, clusterSize, maxClusterSize, zone); } private void validateKubernetesClusterUpgradeParameters(UpgradeKubernetesClusterCmd cmd) { diff --git a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterResourceModifierActionWorker.java b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterResourceModifierActionWorker.java index 6949c0c0b1c..bc06d16e8f9 100644 --- a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterResourceModifierActionWorker.java +++ b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterResourceModifierActionWorker.java @@ -17,6 +17,29 @@ package com.cloud.kubernetes.cluster.actionworkers; +import static com.cloud.utils.NumbersUtil.toHumanReadableSize; + +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import javax.inject.Inject; + +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.command.user.firewall.CreateFirewallRuleCmd; +import org.apache.cloudstack.api.command.user.vm.StartVMCmd; +import org.apache.cloudstack.api.command.user.volume.ResizeVolumeCmd; +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.log4j.Level; + import com.cloud.capacity.CapacityManager; import com.cloud.dc.ClusterDetailsDao; import com.cloud.dc.ClusterDetailsVO; @@ -77,28 +100,6 @@ import com.cloud.vm.VirtualMachine; import com.cloud.vm.VmDetailConstants; import com.cloud.vm.dao.VMInstanceDao; -import org.apache.cloudstack.api.ApiConstants; -import org.apache.cloudstack.api.BaseCmd; -import org.apache.cloudstack.api.command.user.firewall.CreateFirewallRuleCmd; -import org.apache.cloudstack.api.command.user.vm.StartVMCmd; -import org.apache.cloudstack.api.command.user.volume.ResizeVolumeCmd; -import org.apache.commons.codec.binary.Base64; -import org.apache.commons.collections.CollectionUtils; -import org.apache.commons.lang3.StringUtils; -import org.apache.log4j.Level; - -import javax.inject.Inject; -import java.io.File; -import java.io.IOException; -import java.lang.reflect.Field; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -import static com.cloud.utils.NumbersUtil.toHumanReadableSize; - public class KubernetesClusterResourceModifierActionWorker extends KubernetesClusterActionWorker { @Inject @@ -669,7 +670,6 @@ public class KubernetesClusterResourceModifierActionWorker extends KubernetesClu } finally { // Deploying the autoscaler might fail but it can be deployed manually too, so no need to go to an alert state updateLoginUserDetails(null); - stateTransitTo(kubernetesCluster.getId(), KubernetesCluster.Event.OperationSucceeded); } } } diff --git a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterScaleWorker.java b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterScaleWorker.java index a335757450b..b6c9f52640e 100644 --- a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterScaleWorker.java +++ b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterScaleWorker.java @@ -28,6 +28,7 @@ import javax.inject.Inject; import org.apache.cloudstack.api.InternalIdentity; import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang3.StringUtils; import org.apache.log4j.Level; import com.cloud.dc.DataCenter; @@ -57,7 +58,6 @@ import com.cloud.vm.UserVmVO; import com.cloud.vm.VMInstanceVO; import com.cloud.vm.VirtualMachine; import com.cloud.vm.dao.VMInstanceDao; -import org.apache.commons.lang3.StringUtils; public class KubernetesClusterScaleWorker extends KubernetesClusterResourceModifierActionWorker { @@ -406,6 +406,19 @@ public class KubernetesClusterScaleWorker extends KubernetesClusterResourceModif kubernetesCluster = updateKubernetesClusterEntry(clusterSize, null); } + private boolean isAutoscalingChanged() { + if (this.isAutoscalingEnabled == null) { + return false; + } + if (this.isAutoscalingEnabled != kubernetesCluster.getAutoscalingEnabled()) { + return true; + } + if (minSize != null && (!minSize.equals(kubernetesCluster.getMinSize()))) { + return true; + } + return maxSize != null && (!maxSize.equals(kubernetesCluster.getMaxSize())); + } + public boolean scaleCluster() throws CloudRuntimeException { init(); if (LOGGER.isInfoEnabled()) { @@ -417,11 +430,17 @@ public class KubernetesClusterScaleWorker extends KubernetesClusterResourceModif if (existingServiceOffering == null) { logAndThrow(Level.ERROR, String.format("Scaling Kubernetes cluster : %s failed, service offering for the Kubernetes cluster not found!", kubernetesCluster.getName())); } - - if (this.isAutoscalingEnabled != null) { - return autoscaleCluster(this.isAutoscalingEnabled, minSize, maxSize); - } + final boolean autscalingChanged = isAutoscalingChanged(); final boolean serviceOfferingScalingNeeded = serviceOffering != null && serviceOffering.getId() != existingServiceOffering.getId(); + + if (autscalingChanged) { + boolean autoScaled = autoscaleCluster(this.isAutoscalingEnabled, minSize, maxSize); + if (autoScaled && serviceOfferingScalingNeeded) { + scaleKubernetesClusterOffering(); + } + stateTransitTo(kubernetesCluster.getId(), KubernetesCluster.Event.OperationSucceeded); + return autoScaled; + } final boolean clusterSizeScalingNeeded = clusterSize != null && clusterSize != originalClusterSize; final long newVMRequired = clusterSize == null ? 0 : clusterSize - originalClusterSize; if (serviceOfferingScalingNeeded && clusterSizeScalingNeeded) { diff --git a/plugins/integrations/kubernetes-service/src/test/java/com/cloud/kubernetes/cluster/KubernetesClusterManagerImplTest.java b/plugins/integrations/kubernetes-service/src/test/java/com/cloud/kubernetes/cluster/KubernetesClusterManagerImplTest.java new file mode 100644 index 00000000000..8475c9ebaf6 --- /dev/null +++ b/plugins/integrations/kubernetes-service/src/test/java/com/cloud/kubernetes/cluster/KubernetesClusterManagerImplTest.java @@ -0,0 +1,129 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package com.cloud.kubernetes.cluster; + +import java.util.List; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; + +import com.cloud.api.query.dao.TemplateJoinDao; +import com.cloud.api.query.vo.TemplateJoinVO; +import com.cloud.dc.DataCenter; +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.exception.PermissionDeniedException; +import com.cloud.storage.VMTemplateVO; +import com.cloud.storage.dao.VMTemplateDao; + +@RunWith(MockitoJUnitRunner.class) +public class KubernetesClusterManagerImplTest { + + @Mock + VMTemplateDao templateDao; + + @Mock + TemplateJoinDao templateJoinDao; + + @Spy + @InjectMocks + KubernetesClusterManagerImpl clusterManager; + + @Test + public void testValidateKubernetesClusterScaleSizeNullNewSizeNoError() { + clusterManager.validateKubernetesClusterScaleSize(Mockito.mock(KubernetesClusterVO.class), null, 100, Mockito.mock(DataCenter.class)); + } + + @Test + public void testValidateKubernetesClusterScaleSizeSameNewSizeNoError() { + Long size = 2L; + KubernetesClusterVO clusterVO = Mockito.mock(KubernetesClusterVO.class); + Mockito.when(clusterVO.getNodeCount()).thenReturn(size); + clusterManager.validateKubernetesClusterScaleSize(clusterVO, size, 100, Mockito.mock(DataCenter.class)); + } + + @Test(expected = PermissionDeniedException.class) + public void testValidateKubernetesClusterScaleSizeStoppedCluster() { + Long size = 2L; + KubernetesClusterVO clusterVO = Mockito.mock(KubernetesClusterVO.class); + Mockito.when(clusterVO.getNodeCount()).thenReturn(size); + Mockito.when(clusterVO.getState()).thenReturn(KubernetesCluster.State.Stopped); + clusterManager.validateKubernetesClusterScaleSize(clusterVO, 3L, 100, Mockito.mock(DataCenter.class)); + } + + @Test(expected = InvalidParameterValueException.class) + public void testValidateKubernetesClusterScaleSizeZeroNewSize() { + Long size = 2L; + KubernetesClusterVO clusterVO = Mockito.mock(KubernetesClusterVO.class); + Mockito.when(clusterVO.getState()).thenReturn(KubernetesCluster.State.Running); + Mockito.when(clusterVO.getNodeCount()).thenReturn(size); + clusterManager.validateKubernetesClusterScaleSize(clusterVO, 0L, 100, Mockito.mock(DataCenter.class)); + } + + @Test(expected = InvalidParameterValueException.class) + public void testValidateKubernetesClusterScaleSizeOverMaxSize() { + KubernetesClusterVO clusterVO = Mockito.mock(KubernetesClusterVO.class); + Mockito.when(clusterVO.getState()).thenReturn(KubernetesCluster.State.Running); + Mockito.when(clusterVO.getControlNodeCount()).thenReturn(1L); + clusterManager.validateKubernetesClusterScaleSize(clusterVO, 4L, 4, Mockito.mock(DataCenter.class)); + } + + @Test + public void testValidateKubernetesClusterScaleSizeDownsacaleNoError() { + KubernetesClusterVO clusterVO = Mockito.mock(KubernetesClusterVO.class); + Mockito.when(clusterVO.getState()).thenReturn(KubernetesCluster.State.Running); + Mockito.when(clusterVO.getControlNodeCount()).thenReturn(1L); + Mockito.when(clusterVO.getNodeCount()).thenReturn(4L); + clusterManager.validateKubernetesClusterScaleSize(clusterVO, 2L, 10, Mockito.mock(DataCenter.class)); + } + + @Test(expected = InvalidParameterValueException.class) + public void testValidateKubernetesClusterScaleSizeUpscaleDeletedTemplate() { + KubernetesClusterVO clusterVO = Mockito.mock(KubernetesClusterVO.class); + Mockito.when(clusterVO.getState()).thenReturn(KubernetesCluster.State.Running); + Mockito.when(clusterVO.getControlNodeCount()).thenReturn(1L); + Mockito.when(clusterVO.getNodeCount()).thenReturn(2L); + Mockito.when(templateDao.findById(Mockito.anyLong())).thenReturn(null); + clusterManager.validateKubernetesClusterScaleSize(clusterVO, 4L, 10, Mockito.mock(DataCenter.class)); + } + + @Test(expected = InvalidParameterValueException.class) + public void testValidateKubernetesClusterScaleSizeUpscaleNotInZoneTemplate() { + KubernetesClusterVO clusterVO = Mockito.mock(KubernetesClusterVO.class); + Mockito.when(clusterVO.getState()).thenReturn(KubernetesCluster.State.Running); + Mockito.when(clusterVO.getControlNodeCount()).thenReturn(1L); + Mockito.when(clusterVO.getNodeCount()).thenReturn(2L); + Mockito.when(templateDao.findById(Mockito.anyLong())).thenReturn(Mockito.mock(VMTemplateVO.class)); + Mockito.when(templateJoinDao.newTemplateView(Mockito.any(VMTemplateVO.class), Mockito.anyLong(), Mockito.anyBoolean())).thenReturn(null); + clusterManager.validateKubernetesClusterScaleSize(clusterVO, 4L, 10, Mockito.mock(DataCenter.class)); + } + + @Test + public void testValidateKubernetesClusterScaleSizeUpscaleNoError() { + KubernetesClusterVO clusterVO = Mockito.mock(KubernetesClusterVO.class); + Mockito.when(clusterVO.getState()).thenReturn(KubernetesCluster.State.Running); + Mockito.when(clusterVO.getControlNodeCount()).thenReturn(1L); + Mockito.when(clusterVO.getNodeCount()).thenReturn(2L); + Mockito.when(templateDao.findById(Mockito.anyLong())).thenReturn(Mockito.mock(VMTemplateVO.class)); + Mockito.when(templateJoinDao.newTemplateView(Mockito.any(VMTemplateVO.class), Mockito.anyLong(), Mockito.anyBoolean())).thenReturn(List.of(Mockito.mock(TemplateJoinVO.class))); + clusterManager.validateKubernetesClusterScaleSize(clusterVO, 4L, 10, Mockito.mock(DataCenter.class)); + } +} \ No newline at end of file diff --git a/ui/src/config/section/compute.js b/ui/src/config/section/compute.js index e933baf8651..5393e8bd43e 100644 --- a/ui/src/config/section/compute.js +++ b/ui/src/config/section/compute.js @@ -504,7 +504,7 @@ export default { message: 'message.kubernetes.cluster.scale', docHelp: 'plugins/cloudstack-kubernetes-service.html#scaling-kubernetes-cluster', dataView: true, - show: (record) => { return ['Created', 'Running'].includes(record.state) }, + show: (record) => { return ['Created', 'Running', 'Stopped'].includes(record.state) }, popup: true, component: shallowRef(defineAsyncComponent(() => import('@/views/compute/ScaleKubernetesCluster.vue'))) }, diff --git a/ui/src/views/compute/ScaleKubernetesCluster.vue b/ui/src/views/compute/ScaleKubernetesCluster.vue index 17f605d4152..f9af2efac62 100644 --- a/ui/src/views/compute/ScaleKubernetesCluster.vue +++ b/ui/src/views/compute/ScaleKubernetesCluster.vue @@ -28,6 +28,25 @@ :rules="rules" @finish="handleSubmit" layout="vertical"> + + + + + {{ opt.name || opt.description }} + + +