Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ enum KubernetesClusterNodeType {
ControlledEntity findByVmId(long vmId);
void checkVmCanBeDestroyed(UserVm userVm);
void checkVmAffinityGroupsCanBeUpdated(UserVm userVm);
void checkVmServiceOfferingCanBeChanged(UserVm userVm);
boolean isValidNodeType(String nodeType);
Map<String, Long> getServiceOfferingNodeTypeMap(Map<String, Map<String, String>> serviceOfferingNodeTypeMap);
Map<String, Long> getTemplateNodeTypeMap(Map<String, Map<String, String>> templateNodeTypeMap);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,10 @@ public class UserVmResponse extends BaseResponseWithTagInformation implements Co
@Param(description = "User VM type", since = "4.20.0")
private String vmType;

@SerializedName("kubernetesclustertype")
@Param(description = "Management type of the Kubernetes cluster containing this instance")
private String kubernetesClusterType;

@SerializedName(ApiConstants.ARCH)
@Param(description = "CPU arch of the VM", since = "4.20.1")
private String arch;
Expand Down Expand Up @@ -1326,6 +1330,14 @@ public void setVmType(String vmType) {
this.vmType = vmType;
}

public void setKubernetesClusterType(String kubernetesClusterType) {
this.kubernetesClusterType = kubernetesClusterType;
}

public String getKubernetesClusterType() {
return kubernetesClusterType;
}

public String getVmType() {
return vmType;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,25 @@ public void checkVmAffinityGroupsCanBeUpdated(UserVm userVm) {
throw new CloudRuntimeException(errorMessage);
}

@Override
public void checkVmServiceOfferingCanBeChanged(UserVm userVm) {
// Adopted CloudManaged nodes need not have the cksnode VM type.
KubernetesClusterVmMapVO vmMap = kubernetesClusterVmMapDao.findByVmId(userVm.getId());
if (vmMap == null) {
return;
}
KubernetesCluster cluster = kubernetesClusterDao.findById(vmMap.getClusterId());
if (cluster != null && KubernetesCluster.ClusterType.ExternalManaged.equals(cluster.getClusterType())) {
return;
}
String message = "Compute offering cannot be changed for an instance belonging to a Kubernetes cluster";
if (cluster != null) {
message += String.format(": %s", cluster.getName());
}
// A parameter error, so the API also shows the reason to non-admin callers.
throw new InvalidParameterValueException(message + ". Use the Kubernetes cluster scale operation instead.");
}

@Override
public boolean isValidNodeType(String nodeType) {
if (StringUtils.isBlank(nodeType)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,57 @@ public void testCheckVmCanBeDestroyedInExternalManagedCluster() {
kubernetesServiceHelper.checkVmCanBeDestroyed(vm);
}

@Test
public void testOfferingChangeAllowedForOrdinaryVm() {
UserVm vm = Mockito.mock(UserVm.class);
Mockito.when(vm.getId()).thenReturn(1L);
kubernetesServiceHelper.checkVmServiceOfferingCanBeChanged(vm);
Mockito.verify(kubernetesClusterDao, Mockito.never()).findById(Mockito.anyLong());
}

@Test
public void testOfferingChangeAllowedForUnmappedCksVm() {
UserVm vm = Mockito.mock(UserVm.class);
Mockito.when(vm.getId()).thenReturn(1L);
kubernetesServiceHelper.checkVmServiceOfferingCanBeChanged(vm);
}

private UserVm mappedVm(KubernetesCluster.ClusterType type) {
UserVm vm = Mockito.mock(UserVm.class);
Mockito.when(vm.getId()).thenReturn(1L);
KubernetesClusterVmMapVO map = Mockito.mock(KubernetesClusterVmMapVO.class);
Mockito.when(map.getClusterId()).thenReturn(2L);
Mockito.when(kubernetesClusterVmMapDao.findByVmId(1L)).thenReturn(map);
if (type != null) {
KubernetesClusterVO cluster = Mockito.mock(KubernetesClusterVO.class);
Mockito.when(cluster.getClusterType()).thenReturn(type);
Mockito.when(kubernetesClusterDao.findById(2L)).thenReturn(cluster);
}
return vm;
}

@Test(expected = InvalidParameterValueException.class)
public void testOfferingChangeBlockedForCloudManagedNode() {
UserVm vm = mappedVm(KubernetesCluster.ClusterType.CloudManaged);
// The VM type is deliberately not consulted: membership is authoritative.
kubernetesServiceHelper.checkVmServiceOfferingCanBeChanged(vm);
}

@Test(expected = InvalidParameterValueException.class)
public void testOfferingChangeBlockedForAdoptedCloudManagedNode() {
kubernetesServiceHelper.checkVmServiceOfferingCanBeChanged(mappedVm(KubernetesCluster.ClusterType.CloudManaged));
}

@Test
public void testOfferingChangeAllowedForExternalManagedNode() {
kubernetesServiceHelper.checkVmServiceOfferingCanBeChanged(mappedVm(KubernetesCluster.ClusterType.ExternalManaged));
}

@Test(expected = InvalidParameterValueException.class)
public void testOfferingChangeBlockedForMissingMappedCluster() {
kubernetesServiceHelper.checkVmServiceOfferingCanBeChanged(mappedVm(null));
}

@Test
public void testIsValidNodeTypeEmptyNodeType() {
Assert.assertFalse(kubernetesServiceHelper.isValidNodeType(null));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.stereotype.Component;

import com.cloud.api.ApiDBUtils;
Expand All @@ -69,6 +70,8 @@
import com.cloud.host.HostVO;
import com.cloud.host.dao.HostDao;
import com.cloud.host.dao.HostDetailsDao;
import com.cloud.kubernetes.cluster.KubernetesCluster;
import com.cloud.kubernetes.cluster.KubernetesServiceHelper;
import com.cloud.network.IpAddress;
import com.cloud.network.vpc.VpcVO;
import com.cloud.network.vpc.dao.VpcDao;
Expand All @@ -92,6 +95,7 @@
import com.cloud.user.dao.UserDao;
import com.cloud.user.dao.UserStatisticsDao;
import com.cloud.uservm.UserVm;
import com.cloud.utils.component.ComponentContext;
import com.cloud.utils.db.Filter;
import com.cloud.utils.db.SearchBuilder;
import com.cloud.utils.db.SearchCriteria;
Expand Down Expand Up @@ -232,6 +236,7 @@ public UserVmResponse newUserVmResponse(ResponseView view, String objectName, Us
userVmResponse.setLastUpdated(userVm.getLastUpdated());
userVmResponse.setDisplayVm(userVm.isDisplayVm());
userVmResponse.setVmType(userVm.getUserVmType());
populateKubernetesClusterType(userVm.getId(), userVmResponse);

if (userVm.getState() != null) {
userVmResponse.setState(userVm.getState().toString());
Expand Down Expand Up @@ -580,6 +585,21 @@ private long computeLeaseDurationFromExpiryDate(Date created, Date leaseExpiryDa
return ChronoUnit.DAYS.between(createdDate, expiryDate);
}

void populateKubernetesClusterType(long vmId, UserVmResponse response) {
try {
KubernetesServiceHelper helper = ComponentContext.getDelegateComponentOfType(KubernetesServiceHelper.class);
Object cluster = helper.findByVmId(vmId);
if (cluster instanceof KubernetesCluster) {
KubernetesCluster.ClusterType type = ((KubernetesCluster) cluster).getClusterType();
if (type != null) {
response.setKubernetesClusterType(type.name());
}
}
} catch (NoSuchBeanDefinitionException ignored) {
logger.debug("No KubernetesClusterHelper bean found");
}
}

int effectiveCdromMaxCount(UserVmJoinVO userVm) {
Long hostId = userVm.getHostId() != null && userVm.getHostId() > 0
? userVm.getHostId() : userVm.getLastHostId();
Expand Down
15 changes: 15 additions & 0 deletions server/src/main/java/com/cloud/vm/UserVmManagerImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -1303,6 +1303,8 @@ public UserVm upgradeVirtualMachine(UpgradeVMCmd cmd) throws ResourceAllocationE

_accountMgr.checkAccess(caller, null, true, vmInstance);

checkPluginsIfVmServiceOfferingCanBeChanged(_vmDao.findById(vmId));

upgradeStoppedVirtualMachine(vmId, svcOffId, cmd.getDetails());

// Generate usage event for VM upgrade
Expand Down Expand Up @@ -2123,6 +2125,8 @@ public UserVm upgradeVirtualMachine(ScaleVMCmd cmd) throws ResourceUnavailableEx
throw new InvalidParameterValueException(String.format("Operation not supported for instance: %s",
vm.getName()));
}
_accountMgr.checkAccess(CallContext.current().getCallingAccount(), null, true, vm);
checkPluginsIfVmServiceOfferingCanBeChanged(_vmDao.findById(vmId));
CallContext.current().setEventDetails("Vm Id: " + vm.getUuid());

Map<String, String> cmdDetails = cmd.getDetails();
Expand Down Expand Up @@ -3676,6 +3680,17 @@ protected void checkExpungeVmPermission(Account callingAccount, String apiKey) {
}
}

// API boundary only: CKS scaling calls the internal upgradeVirtualMachine overloads.
protected void checkPluginsIfVmServiceOfferingCanBeChanged(UserVm vm) {
try {
KubernetesServiceHelper kubernetesServiceHelper =
ComponentContext.getDelegateComponentOfType(KubernetesServiceHelper.class);
kubernetesServiceHelper.checkVmServiceOfferingCanBeChanged(vm);
} catch (NoSuchBeanDefinitionException ignored) {
logger.debug("No KubernetesClusterHelper bean found");
}
}

protected void checkPluginsIfVmCanBeDestroyed(UserVm vm) {
try {
KubernetesServiceHelper kubernetesServiceHelper =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// 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.api.query.dao;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when;

import com.cloud.kubernetes.cluster.KubernetesCluster;
import com.cloud.kubernetes.cluster.KubernetesServiceHelper;
import com.cloud.utils.component.ComponentContext;
import org.apache.cloudstack.api.response.UserVmResponse;
import org.junit.Test;
import org.mockito.MockedStatic;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;

public class UserVmKubernetesClusterTypeTest {
@Test
public void reportsManagementTypeForUiIncludingAdoptedNodes() {
KubernetesServiceHelper helper = mock(KubernetesServiceHelper.class);
KubernetesCluster cluster = mock(KubernetesCluster.class);
when(helper.findByVmId(1L)).thenReturn(cluster);
try (MockedStatic<ComponentContext> components = mockStatic(ComponentContext.class)) {
components.when(() -> ComponentContext.getDelegateComponentOfType(KubernetesServiceHelper.class)).thenReturn(helper);
for (KubernetesCluster.ClusterType type : KubernetesCluster.ClusterType.values()) {
when(cluster.getClusterType()).thenReturn(type);
UserVmResponse response = new UserVmResponse();
new UserVmJoinDaoImpl().populateKubernetesClusterType(1L, response);
assertEquals(type.name(), response.getKubernetesClusterType());
}
}
}

@Test
public void ordinaryVmHasNoClusterType() {
KubernetesServiceHelper helper = mock(KubernetesServiceHelper.class);
try (MockedStatic<ComponentContext> components = mockStatic(ComponentContext.class)) {
components.when(() -> ComponentContext.getDelegateComponentOfType(KubernetesServiceHelper.class)).thenReturn(helper);
UserVmResponse response = new UserVmResponse();
new UserVmJoinDaoImpl().populateKubernetesClusterType(1L, response);
assertNull(response.getKubernetesClusterType());
}
}

@Test
public void absentPluginHasNoClusterType() {
try (MockedStatic<ComponentContext> components = mockStatic(ComponentContext.class)) {
components.when(() -> ComponentContext.getDelegateComponentOfType(KubernetesServiceHelper.class))
.thenThrow(new NoSuchBeanDefinitionException(KubernetesServiceHelper.class));
UserVmResponse response = new UserVmResponse();
new UserVmJoinDaoImpl().populateKubernetesClusterType(1L, response);
assertNull(response.getKubernetesClusterType());
}
}
}
Loading
Loading