From fe0a2a33971f8f3592ee0640ebdcb870926af40e Mon Sep 17 00:00:00 2001 From: Daman Arora Date: Tue, 6 Jan 2026 08:51:43 -0500 Subject: [PATCH 01/26] Add NODE_TYPE_AFFINITY_GROUP_MAP constant and affinity group mapping to CreateKubernetesClusterCmd --- .../main/java/org/apache/cloudstack/api/ApiConstants.java | 1 + .../user/kubernetes/cluster/CreateKubernetesClusterCmd.java | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java index 8fca652518f2..1525409fcb38 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -1235,6 +1235,7 @@ public class ApiConstants { public static final String MAX_SIZE = "maxsize"; public static final String NODE_TYPE_OFFERING_MAP = "nodeofferings"; public static final String NODE_TYPE_TEMPLATE_MAP = "nodetemplates"; + public static final String NODE_TYPE_AFFINITY_GROUP_MAP = "nodeaffinitygroups"; public static final String BOOT_TYPE = "boottype"; public static final String BOOT_MODE = "bootmode"; diff --git a/plugins/integrations/kubernetes-service/src/main/java/org/apache/cloudstack/api/command/user/kubernetes/cluster/CreateKubernetesClusterCmd.java b/plugins/integrations/kubernetes-service/src/main/java/org/apache/cloudstack/api/command/user/kubernetes/cluster/CreateKubernetesClusterCmd.java index ad4f61f3e9b1..397aa14ab7fe 100644 --- a/plugins/integrations/kubernetes-service/src/main/java/org/apache/cloudstack/api/command/user/kubernetes/cluster/CreateKubernetesClusterCmd.java +++ b/plugins/integrations/kubernetes-service/src/main/java/org/apache/cloudstack/api/command/user/kubernetes/cluster/CreateKubernetesClusterCmd.java @@ -125,6 +125,12 @@ public class CreateKubernetesClusterCmd extends BaseAsyncCreateCmd { since = "4.21.0") private Map> templateNodeTypeMap; + @ACL(accessType = AccessType.UseEntry) + @Parameter(name = ApiConstants.NODE_TYPE_AFFINITY_GROUP_MAP, type = CommandType.MAP, + description = "(Optional) Node Type to Affinity Group ID mapping. If provided, VMs of each node type will be added to the specified affinity group", + since = "4.23.0") + private Map> affinityGroupNodeTypeMap; + @ACL(accessType = AccessType.UseEntry) @Parameter(name = ApiConstants.ETCD_NODES, type = CommandType.LONG, description = "(Optional) Number of Kubernetes cluster etcd nodes, default is 0." + From a7e527033600010afd4f87d495a3ef2558d593dd Mon Sep 17 00:00:00 2001 From: Daman Arora Date: Tue, 6 Jan 2026 10:26:41 -0500 Subject: [PATCH 02/26] Implement getAffinityGroupNodeTypeMap in kubernetes service helper --- .../cluster/KubernetesServiceHelper.java | 1 + .../java/com/cloud/vm/VmDetailConstants.java | 1 + .../cluster/KubernetesServiceHelperImpl.java | 60 +++++++++++++++++++ .../cluster/CreateKubernetesClusterCmd.java | 10 +++- 4 files changed, 69 insertions(+), 3 deletions(-) diff --git a/api/src/main/java/com/cloud/kubernetes/cluster/KubernetesServiceHelper.java b/api/src/main/java/com/cloud/kubernetes/cluster/KubernetesServiceHelper.java index 37b8907b454a..a9f76c174b59 100644 --- a/api/src/main/java/com/cloud/kubernetes/cluster/KubernetesServiceHelper.java +++ b/api/src/main/java/com/cloud/kubernetes/cluster/KubernetesServiceHelper.java @@ -36,5 +36,6 @@ enum KubernetesClusterNodeType { boolean isValidNodeType(String nodeType); Map getServiceOfferingNodeTypeMap(Map> serviceOfferingNodeTypeMap); Map getTemplateNodeTypeMap(Map> templateNodeTypeMap); + Map getAffinityGroupNodeTypeMap(Map> affinityGroupNodeTypeMap); void cleanupForAccount(Account account); } diff --git a/api/src/main/java/com/cloud/vm/VmDetailConstants.java b/api/src/main/java/com/cloud/vm/VmDetailConstants.java index 596c861218f0..217e9f9224c4 100644 --- a/api/src/main/java/com/cloud/vm/VmDetailConstants.java +++ b/api/src/main/java/com/cloud/vm/VmDetailConstants.java @@ -93,6 +93,7 @@ public interface VmDetailConstants { String CKS_NODE_TYPE = "node"; String OFFERING = "offering"; String TEMPLATE = "template"; + String AFFINITY_GROUP = "affinitygroup"; // VMware to KVM VM migrations specific String VMWARE_TO_KVM_PREFIX = "vmware-to-kvm"; diff --git a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/KubernetesServiceHelperImpl.java b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/KubernetesServiceHelperImpl.java index 30465c99780d..62e190b35bd4 100644 --- a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/KubernetesServiceHelperImpl.java +++ b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/KubernetesServiceHelperImpl.java @@ -24,6 +24,8 @@ import javax.inject.Inject; +import org.apache.cloudstack.affinity.AffinityGroup; +import org.apache.cloudstack.affinity.dao.AffinityGroupDao; import com.cloud.exception.InvalidParameterValueException; import com.cloud.offering.ServiceOffering; import com.cloud.service.dao.ServiceOfferingDao; @@ -66,6 +68,8 @@ public class KubernetesServiceHelperImpl extends AdapterBase implements Kubernet @Inject protected VMTemplateDao vmTemplateDao; @Inject + protected AffinityGroupDao affinityGroupDao; + @Inject KubernetesClusterService kubernetesClusterService; protected void setEventTypeEntityDetails(Class eventTypeDefinedClass, Class entityClass) { @@ -244,6 +248,62 @@ public Map getTemplateNodeTypeMap(Map> return mapping; } + protected void checkNodeTypeAffinityGroupEntryCompleteness(String nodeTypeStr, String affinityGroupUuid) { + if (StringUtils.isAnyBlank(nodeTypeStr, affinityGroupUuid)) { + String error = String.format("Any Node Type to Affinity Group entry should have a valid '%s' and '%s' values", + VmDetailConstants.CKS_NODE_TYPE, VmDetailConstants.AFFINITY_GROUP); + logger.error(error); + throw new InvalidParameterValueException(error); + } + } + + protected void checkNodeTypeAffinityGroupEntryValues(String nodeTypeStr, AffinityGroup affinityGroup, String affinityGroupUuid) { + if (!isValidNodeType(nodeTypeStr)) { + String error = String.format("The provided value '%s' for Node Type is invalid", nodeTypeStr); + logger.error(error); + throw new InvalidParameterValueException(String.format(error)); + } + if (affinityGroup == null) { + String error = String.format("Cannot find an affinity group with ID %s", affinityGroupUuid); + logger.error(error); + throw new InvalidParameterValueException(error); + } + } + + protected void addNodeTypeAffinityGroupEntry(String nodeTypeStr, String affinityGroupUuid, AffinityGroup affinityGroup, Map mapping) { + if (logger.isDebugEnabled()) { + logger.debug(String.format("Node Type: '%s' should use affinity group ID: '%s'", nodeTypeStr, affinityGroupUuid)); + } + KubernetesClusterNodeType nodeType = KubernetesClusterNodeType.valueOf(nodeTypeStr.toUpperCase()); + mapping.put(nodeType.name(), affinityGroup.getId()); + } + + protected void processNodeTypeAffinityGroupEntryAndAddToMappingIfValid(Map entry, Map mapping) { + if (MapUtils.isEmpty(entry)) { + return; + } + String nodeTypeStr = entry.get(VmDetailConstants.CKS_NODE_TYPE); + String affinityGroupUuid = entry.get(VmDetailConstants.AFFINITY_GROUP); + checkNodeTypeAffinityGroupEntryCompleteness(nodeTypeStr, affinityGroupUuid); + + AffinityGroup affinityGroup = affinityGroupDao.findByUuid(affinityGroupUuid); + checkNodeTypeAffinityGroupEntryValues(nodeTypeStr, affinityGroup, affinityGroupUuid); + + addNodeTypeAffinityGroupEntry(nodeTypeStr, affinityGroupUuid, affinityGroup, mapping); + } + + @Override + public Map getAffinityGroupNodeTypeMap(Map> affinityGroupNodeTypeMap) { + Map mapping = new HashMap<>(); + if (MapUtils.isNotEmpty(affinityGroupNodeTypeMap)) { + for (Map entry : affinityGroupNodeTypeMap.values()) { + processNodeTypeAffinityGroupEntryAndAddToMappingIfValid(entry, mapping); + } + } + return mapping; + } + + public void cleanupForAccount(Account account) { kubernetesClusterService.cleanupForAccount(account); } diff --git a/plugins/integrations/kubernetes-service/src/main/java/org/apache/cloudstack/api/command/user/kubernetes/cluster/CreateKubernetesClusterCmd.java b/plugins/integrations/kubernetes-service/src/main/java/org/apache/cloudstack/api/command/user/kubernetes/cluster/CreateKubernetesClusterCmd.java index be68b33f1fe1..4e458bbe6d72 100644 --- a/plugins/integrations/kubernetes-service/src/main/java/org/apache/cloudstack/api/command/user/kubernetes/cluster/CreateKubernetesClusterCmd.java +++ b/plugins/integrations/kubernetes-service/src/main/java/org/apache/cloudstack/api/command/user/kubernetes/cluster/CreateKubernetesClusterCmd.java @@ -79,7 +79,7 @@ public class CreateKubernetesClusterCmd extends BaseAsyncCreateCmd { @Inject public KubernetesClusterService kubernetesClusterService; @Inject - protected KubernetesServiceHelper kubernetesClusterHelper; + protected KubernetesServiceHelper kubernetesServiceHelper; @Inject private ConfigurationDao configurationDao; @Inject @@ -320,11 +320,15 @@ public String getClusterType() { } public Map getServiceOfferingNodeTypeMap() { - return kubernetesClusterHelper.getServiceOfferingNodeTypeMap(serviceOfferingNodeTypeMap); + return kubernetesServiceHelper.getServiceOfferingNodeTypeMap(serviceOfferingNodeTypeMap); } public Map getTemplateNodeTypeMap() { - return kubernetesClusterHelper.getTemplateNodeTypeMap(templateNodeTypeMap); + return kubernetesServiceHelper.getTemplateNodeTypeMap(templateNodeTypeMap); + } + + public Map getAffinityGroupNodeTypeMap() { + return kubernetesServiceHelper.getAffinityGroupNodeTypeMap(affinityGroupNodeTypeMap); } public Hypervisor.HypervisorType getHypervisorType() { From 58804a39a77912e2416578c4b94edb77dbf586be Mon Sep 17 00:00:00 2001 From: Daman Arora Date: Tue, 6 Jan 2026 10:26:51 -0500 Subject: [PATCH 03/26] Rename kubernetesClusterHelper to kubernetesServiceHelper for consistency --- .../user/kubernetes/cluster/ScaleKubernetesClusterCmd.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/integrations/kubernetes-service/src/main/java/org/apache/cloudstack/api/command/user/kubernetes/cluster/ScaleKubernetesClusterCmd.java b/plugins/integrations/kubernetes-service/src/main/java/org/apache/cloudstack/api/command/user/kubernetes/cluster/ScaleKubernetesClusterCmd.java index c7ee0b7da92a..1cff2649428d 100644 --- a/plugins/integrations/kubernetes-service/src/main/java/org/apache/cloudstack/api/command/user/kubernetes/cluster/ScaleKubernetesClusterCmd.java +++ b/plugins/integrations/kubernetes-service/src/main/java/org/apache/cloudstack/api/command/user/kubernetes/cluster/ScaleKubernetesClusterCmd.java @@ -57,7 +57,7 @@ public class ScaleKubernetesClusterCmd extends BaseAsyncCmd { @Inject public KubernetesClusterService kubernetesClusterService; @Inject - protected KubernetesServiceHelper kubernetesClusterHelper; + protected KubernetesServiceHelper kubernetesServiceHelper; ///////////////////////////////////////////////////// //////////////// API parameters ///////////////////// @@ -114,7 +114,7 @@ public Long getServiceOfferingId() { } public Map getServiceOfferingNodeTypeMap() { - return kubernetesClusterHelper.getServiceOfferingNodeTypeMap(this.serviceOfferingNodeTypeMap); + return kubernetesServiceHelper.getServiceOfferingNodeTypeMap(this.serviceOfferingNodeTypeMap); } public Long getClusterSize() { From 1114f759c2c26d2c73900723b0cc216f08a6e783 Mon Sep 17 00:00:00 2001 From: Daman Arora Date: Tue, 6 Jan 2026 10:27:03 -0500 Subject: [PATCH 04/26] Refactor KubernetesServiceHelperImplTest to include affinity group handling and enhance node type validation tests --- .../KubernetesClusterHelperImplTest.java | 145 ------------ .../KubernetesServiceHelperImplTest.java | 210 ++++++++++++++++++ 2 files changed, 210 insertions(+), 145 deletions(-) delete mode 100644 plugins/integrations/kubernetes-service/src/test/java/com/cloud/kubernetes/cluster/KubernetesClusterHelperImplTest.java diff --git a/plugins/integrations/kubernetes-service/src/test/java/com/cloud/kubernetes/cluster/KubernetesClusterHelperImplTest.java b/plugins/integrations/kubernetes-service/src/test/java/com/cloud/kubernetes/cluster/KubernetesClusterHelperImplTest.java deleted file mode 100644 index 298f1dfbcd61..000000000000 --- a/plugins/integrations/kubernetes-service/src/test/java/com/cloud/kubernetes/cluster/KubernetesClusterHelperImplTest.java +++ /dev/null @@ -1,145 +0,0 @@ -// 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 com.cloud.exception.InvalidParameterValueException; -import com.cloud.service.ServiceOfferingVO; -import com.cloud.service.dao.ServiceOfferingDao; -import com.cloud.vm.VmDetailConstants; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.junit.MockitoJUnitRunner; - -import java.util.HashMap; -import java.util.Map; -import java.util.UUID; - -import static com.cloud.kubernetes.cluster.KubernetesServiceHelper.KubernetesClusterNodeType.CONTROL; -import static com.cloud.kubernetes.cluster.KubernetesServiceHelper.KubernetesClusterNodeType.ETCD; -import static com.cloud.kubernetes.cluster.KubernetesServiceHelper.KubernetesClusterNodeType.WORKER; - -@RunWith(MockitoJUnitRunner.class) -public class KubernetesClusterHelperImplTest { - - @Mock - private ServiceOfferingDao serviceOfferingDao; - @Mock - private ServiceOfferingVO workerServiceOffering; - @Mock - private ServiceOfferingVO controlServiceOffering; - @Mock - private ServiceOfferingVO etcdServiceOffering; - - private static final String workerNodesOfferingId = UUID.randomUUID().toString(); - private static final String controlNodesOfferingId = UUID.randomUUID().toString(); - private static final String etcdNodesOfferingId = UUID.randomUUID().toString(); - private static final Long workerOfferingId = 1L; - private static final Long controlOfferingId = 2L; - private static final Long etcdOfferingId = 3L; - - private final KubernetesServiceHelperImpl helper = new KubernetesServiceHelperImpl(); - - @Before - public void setUp() { - helper.serviceOfferingDao = serviceOfferingDao; - Mockito.when(serviceOfferingDao.findByUuid(workerNodesOfferingId)).thenReturn(workerServiceOffering); - Mockito.when(serviceOfferingDao.findByUuid(controlNodesOfferingId)).thenReturn(controlServiceOffering); - Mockito.when(serviceOfferingDao.findByUuid(etcdNodesOfferingId)).thenReturn(etcdServiceOffering); - Mockito.when(workerServiceOffering.getId()).thenReturn(workerOfferingId); - Mockito.when(controlServiceOffering.getId()).thenReturn(controlOfferingId); - Mockito.when(etcdServiceOffering.getId()).thenReturn(etcdOfferingId); - } - - @Test - public void testIsValidNodeTypeEmptyNodeType() { - Assert.assertFalse(helper.isValidNodeType(null)); - } - - @Test - public void testIsValidNodeTypeInvalidNodeType() { - String nodeType = "invalidNodeType"; - Assert.assertFalse(helper.isValidNodeType(nodeType)); - } - - @Test - public void testIsValidNodeTypeValidNodeTypeLowercase() { - String nodeType = KubernetesServiceHelper.KubernetesClusterNodeType.WORKER.name().toLowerCase(); - Assert.assertTrue(helper.isValidNodeType(nodeType)); - } - - private Map createMapEntry(KubernetesServiceHelper.KubernetesClusterNodeType nodeType, - String nodeTypeOfferingUuid) { - Map map = new HashMap<>(); - map.put(VmDetailConstants.CKS_NODE_TYPE, nodeType.name().toLowerCase()); - map.put(VmDetailConstants.OFFERING, nodeTypeOfferingUuid); - return map; - } - - @Test - public void testNodeOfferingMap() { - Map> serviceOfferingNodeTypeMap = new HashMap<>(); - Map firstMap = createMapEntry(WORKER, workerNodesOfferingId); - Map secondMap = createMapEntry(CONTROL, controlNodesOfferingId); - serviceOfferingNodeTypeMap.put("map1", firstMap); - serviceOfferingNodeTypeMap.put("map2", secondMap); - Map map = helper.getServiceOfferingNodeTypeMap(serviceOfferingNodeTypeMap); - Assert.assertNotNull(map); - Assert.assertEquals(2, map.size()); - Assert.assertTrue(map.containsKey(WORKER.name()) && map.containsKey(CONTROL.name())); - Assert.assertEquals(workerOfferingId, map.get(WORKER.name())); - Assert.assertEquals(controlOfferingId, map.get(CONTROL.name())); - } - - @Test - public void testNodeOfferingMapNullMap() { - Map map = helper.getServiceOfferingNodeTypeMap(null); - Assert.assertTrue(map.isEmpty()); - } - - @Test - public void testNodeOfferingMapEtcdNodes() { - Map> serviceOfferingNodeTypeMap = new HashMap<>(); - Map firstMap = createMapEntry(ETCD, etcdNodesOfferingId); - serviceOfferingNodeTypeMap.put("map1", firstMap); - Map map = helper.getServiceOfferingNodeTypeMap(serviceOfferingNodeTypeMap); - Assert.assertNotNull(map); - Assert.assertEquals(1, map.size()); - Assert.assertTrue(map.containsKey(ETCD.name())); - Assert.assertEquals(etcdOfferingId, map.get(ETCD.name())); - } - - @Test(expected = InvalidParameterValueException.class) - public void testCheckNodeTypeOfferingEntryCompletenessInvalidParameters() { - helper.checkNodeTypeOfferingEntryCompleteness(WORKER.name(), null); - } - - @Test(expected = InvalidParameterValueException.class) - public void testCheckNodeTypeOfferingEntryValuesInvalidNodeType() { - String invalidNodeType = "invalidNodeTypeName"; - helper.checkNodeTypeOfferingEntryValues(invalidNodeType, workerServiceOffering, workerNodesOfferingId); - } - - @Test(expected = InvalidParameterValueException.class) - public void testCheckNodeTypeOfferingEntryValuesEmptyOffering() { - String nodeType = WORKER.name(); - helper.checkNodeTypeOfferingEntryValues(nodeType, null, workerNodesOfferingId); - } -} diff --git a/plugins/integrations/kubernetes-service/src/test/java/com/cloud/kubernetes/cluster/KubernetesServiceHelperImplTest.java b/plugins/integrations/kubernetes-service/src/test/java/com/cloud/kubernetes/cluster/KubernetesServiceHelperImplTest.java index 3e6688e87577..1ce8a99bd561 100644 --- a/plugins/integrations/kubernetes-service/src/test/java/com/cloud/kubernetes/cluster/KubernetesServiceHelperImplTest.java +++ b/plugins/integrations/kubernetes-service/src/test/java/com/cloud/kubernetes/cluster/KubernetesServiceHelperImplTest.java @@ -17,6 +17,14 @@ package com.cloud.kubernetes.cluster; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +import org.apache.cloudstack.affinity.AffinityGroup; +import org.apache.cloudstack.affinity.AffinityGroupVO; +import org.apache.cloudstack.affinity.dao.AffinityGroupDao; +import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InjectMocks; @@ -24,11 +32,16 @@ import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.kubernetes.cluster.KubernetesServiceHelper.KubernetesClusterNodeType; import com.cloud.kubernetes.cluster.dao.KubernetesClusterDao; import com.cloud.kubernetes.cluster.dao.KubernetesClusterVmMapDao; +import com.cloud.service.ServiceOfferingVO; +import com.cloud.service.dao.ServiceOfferingDao; import com.cloud.uservm.UserVm; import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.vm.UserVmManager; +import com.cloud.vm.VmDetailConstants; @RunWith(MockitoJUnitRunner.class) public class KubernetesServiceHelperImplTest { @@ -36,6 +49,10 @@ public class KubernetesServiceHelperImplTest { KubernetesClusterVmMapDao kubernetesClusterVmMapDao; @Mock KubernetesClusterDao kubernetesClusterDao; + @Mock + AffinityGroupDao affinityGroupDao; + @Mock + ServiceOfferingDao serviceOfferingDao; @InjectMocks KubernetesServiceHelperImpl kubernetesServiceHelper = new KubernetesServiceHelperImpl(); @@ -84,4 +101,197 @@ public void testCheckVmCanBeDestroyedInExternalManagedCluster() { Mockito.when(kubernetesCluster.getClusterType()).thenReturn(KubernetesCluster.ClusterType.ExternalManaged); kubernetesServiceHelper.checkVmCanBeDestroyed(vm); } + + @Test + public void testIsValidNodeTypeEmptyNodeType() { + Assert.assertFalse(kubernetesServiceHelper.isValidNodeType(null)); + } + + @Test + public void testIsValidNodeTypeInvalidNodeType() { + Assert.assertFalse(kubernetesServiceHelper.isValidNodeType("invalidNodeType")); + } + + @Test + public void testIsValidNodeTypeValidNodeTypeLowercase() { + String nodeType = KubernetesClusterNodeType.WORKER.name().toLowerCase(); + Assert.assertTrue(kubernetesServiceHelper.isValidNodeType(nodeType)); + } + + private Map createServiceOfferingMapEntry(KubernetesClusterNodeType nodeType, String offeringUuid) { + Map map = new HashMap<>(); + map.put(VmDetailConstants.CKS_NODE_TYPE, nodeType.name().toLowerCase()); + map.put(VmDetailConstants.OFFERING, offeringUuid); + return map; + } + + @Test + public void testGetServiceOfferingNodeTypeMap() { + String workerOfferingUuid = UUID.randomUUID().toString(); + String controlOfferingUuid = UUID.randomUUID().toString(); + + ServiceOfferingVO workerOffering = Mockito.mock(ServiceOfferingVO.class); + Mockito.when(workerOffering.getId()).thenReturn(1L); + Mockito.when(serviceOfferingDao.findByUuid(workerOfferingUuid)).thenReturn(workerOffering); + + ServiceOfferingVO controlOffering = Mockito.mock(ServiceOfferingVO.class); + Mockito.when(controlOffering.getId()).thenReturn(2L); + Mockito.when(serviceOfferingDao.findByUuid(controlOfferingUuid)).thenReturn(controlOffering); + + Map> serviceOfferingNodeTypeMap = new HashMap<>(); + serviceOfferingNodeTypeMap.put("map1", createServiceOfferingMapEntry(KubernetesClusterNodeType.WORKER, workerOfferingUuid)); + serviceOfferingNodeTypeMap.put("map2", createServiceOfferingMapEntry(KubernetesClusterNodeType.CONTROL, controlOfferingUuid)); + + Map result = kubernetesServiceHelper.getServiceOfferingNodeTypeMap(serviceOfferingNodeTypeMap); + + Assert.assertNotNull(result); + Assert.assertEquals(2, result.size()); + Assert.assertTrue(result.containsKey(KubernetesClusterNodeType.WORKER.name())); + Assert.assertTrue(result.containsKey(KubernetesClusterNodeType.CONTROL.name())); + Assert.assertEquals(Long.valueOf(1L), result.get(KubernetesClusterNodeType.WORKER.name())); + Assert.assertEquals(Long.valueOf(2L), result.get(KubernetesClusterNodeType.CONTROL.name())); + } + + @Test + public void testGetServiceOfferingNodeTypeMapNullMap() { + Map result = kubernetesServiceHelper.getServiceOfferingNodeTypeMap(null); + Assert.assertTrue(result.isEmpty()); + } + + @Test + public void testGetServiceOfferingNodeTypeMapEtcdNodes() { + String etcdOfferingUuid = UUID.randomUUID().toString(); + + ServiceOfferingVO etcdOffering = Mockito.mock(ServiceOfferingVO.class); + Mockito.when(etcdOffering.getId()).thenReturn(3L); + Mockito.when(serviceOfferingDao.findByUuid(etcdOfferingUuid)).thenReturn(etcdOffering); + + Map> serviceOfferingNodeTypeMap = new HashMap<>(); + serviceOfferingNodeTypeMap.put("map1", createServiceOfferingMapEntry(KubernetesClusterNodeType.ETCD, etcdOfferingUuid)); + + Map result = kubernetesServiceHelper.getServiceOfferingNodeTypeMap(serviceOfferingNodeTypeMap); + + Assert.assertNotNull(result); + Assert.assertEquals(1, result.size()); + Assert.assertTrue(result.containsKey(KubernetesClusterNodeType.ETCD.name())); + Assert.assertEquals(Long.valueOf(3L), result.get(KubernetesClusterNodeType.ETCD.name())); + } + + @Test(expected = InvalidParameterValueException.class) + public void testCheckNodeTypeOfferingEntryCompletenessInvalidParameters() { + kubernetesServiceHelper.checkNodeTypeOfferingEntryCompleteness(KubernetesClusterNodeType.WORKER.name(), null); + } + + @Test(expected = InvalidParameterValueException.class) + public void testCheckNodeTypeOfferingEntryValuesInvalidNodeType() { + ServiceOfferingVO offering = Mockito.mock(ServiceOfferingVO.class); + kubernetesServiceHelper.checkNodeTypeOfferingEntryValues("invalidNodeTypeName", offering, "some-uuid"); + } + + @Test(expected = InvalidParameterValueException.class) + public void testCheckNodeTypeOfferingEntryValuesEmptyOffering() { + kubernetesServiceHelper.checkNodeTypeOfferingEntryValues(KubernetesClusterNodeType.WORKER.name(), null, "some-uuid"); + } + + @Test(expected = InvalidParameterValueException.class) + public void testCheckNodeTypeAffinityGroupEntryCompletenessBlankNodeType() { + kubernetesServiceHelper.checkNodeTypeAffinityGroupEntryCompleteness("", "affinity-group-uuid"); + } + + @Test(expected = InvalidParameterValueException.class) + public void testCheckNodeTypeAffinityGroupEntryCompletenessBlankAffinityGroupUuid() { + kubernetesServiceHelper.checkNodeTypeAffinityGroupEntryCompleteness("control", ""); + } + + @Test + public void testCheckNodeTypeAffinityGroupEntryCompletenessValid() { + kubernetesServiceHelper.checkNodeTypeAffinityGroupEntryCompleteness("control", "affinity-group-uuid"); + } + + @Test(expected = InvalidParameterValueException.class) + public void testCheckNodeTypeAffinityGroupEntryValuesInvalidNodeType() { + AffinityGroup affinityGroup = Mockito.mock(AffinityGroup.class); + kubernetesServiceHelper.checkNodeTypeAffinityGroupEntryValues("invalid-node-type", affinityGroup, "affinity-group-uuid"); + } + + @Test(expected = InvalidParameterValueException.class) + public void testCheckNodeTypeAffinityGroupEntryValuesNullAffinityGroup() { + kubernetesServiceHelper.checkNodeTypeAffinityGroupEntryValues("control", null, "affinity-group-uuid"); + } + + @Test + public void testCheckNodeTypeAffinityGroupEntryValuesValid() { + AffinityGroup affinityGroup = Mockito.mock(AffinityGroup.class); + kubernetesServiceHelper.checkNodeTypeAffinityGroupEntryValues("control", affinityGroup, "affinity-group-uuid"); + } + + @Test + public void testAddNodeTypeAffinityGroupEntry() { + AffinityGroup affinityGroup = Mockito.mock(AffinityGroup.class); + Mockito.when(affinityGroup.getId()).thenReturn(100L); + Map mapping = new HashMap<>(); + kubernetesServiceHelper.addNodeTypeAffinityGroupEntry("control", "affinity-group-uuid", affinityGroup, mapping); + Assert.assertEquals(1, mapping.size()); + Assert.assertEquals(Long.valueOf(100L), mapping.get("CONTROL")); + } + + @Test + public void testProcessNodeTypeAffinityGroupEntryAndAddToMappingIfValidEmptyEntry() { + Map mapping = new HashMap<>(); + kubernetesServiceHelper.processNodeTypeAffinityGroupEntryAndAddToMappingIfValid(new HashMap<>(), mapping); + Assert.assertTrue(mapping.isEmpty()); + } + + @Test + public void testProcessNodeTypeAffinityGroupEntryAndAddToMappingIfValidValidEntry() { + AffinityGroupVO affinityGroup = Mockito.mock(AffinityGroupVO.class); + Mockito.when(affinityGroup.getId()).thenReturn(100L); + Mockito.when(affinityGroupDao.findByUuid("affinity-group-uuid")).thenReturn(affinityGroup); + + Map entry = new HashMap<>(); + entry.put(VmDetailConstants.CKS_NODE_TYPE, "control"); + entry.put(VmDetailConstants.AFFINITY_GROUP, "affinity-group-uuid"); + + Map mapping = new HashMap<>(); + kubernetesServiceHelper.processNodeTypeAffinityGroupEntryAndAddToMappingIfValid(entry, mapping); + Assert.assertEquals(1, mapping.size()); + Assert.assertEquals(Long.valueOf(100L), mapping.get("CONTROL")); + } + + @Test + public void testGetAffinityGroupNodeTypeMapEmptyMap() { + Map result = kubernetesServiceHelper.getAffinityGroupNodeTypeMap(null); + Assert.assertTrue(result.isEmpty()); + + result = kubernetesServiceHelper.getAffinityGroupNodeTypeMap(new HashMap<>()); + Assert.assertTrue(result.isEmpty()); + } + + @Test + public void testGetAffinityGroupNodeTypeMapValidEntries() { + AffinityGroupVO controlAffinityGroup = Mockito.mock(AffinityGroupVO.class); + Mockito.when(controlAffinityGroup.getId()).thenReturn(100L); + Mockito.when(affinityGroupDao.findByUuid("control-affinity-uuid")).thenReturn(controlAffinityGroup); + + AffinityGroupVO workerAffinityGroup = Mockito.mock(AffinityGroupVO.class); + Mockito.when(workerAffinityGroup.getId()).thenReturn(200L); + Mockito.when(affinityGroupDao.findByUuid("worker-affinity-uuid")).thenReturn(workerAffinityGroup); + + Map> affinityGroupNodeTypeMap = new HashMap<>(); + + Map controlEntry = new HashMap<>(); + controlEntry.put(VmDetailConstants.CKS_NODE_TYPE, "control"); + controlEntry.put(VmDetailConstants.AFFINITY_GROUP, "control-affinity-uuid"); + affinityGroupNodeTypeMap.put("0", controlEntry); + + Map workerEntry = new HashMap<>(); + workerEntry.put(VmDetailConstants.CKS_NODE_TYPE, "worker"); + workerEntry.put(VmDetailConstants.AFFINITY_GROUP, "worker-affinity-uuid"); + affinityGroupNodeTypeMap.put("1", workerEntry); + + Map result = kubernetesServiceHelper.getAffinityGroupNodeTypeMap(affinityGroupNodeTypeMap); + Assert.assertEquals(2, result.size()); + Assert.assertEquals(Long.valueOf(100L), result.get("CONTROL")); + Assert.assertEquals(Long.valueOf(200L), result.get("WORKER")); + } } From 319c0f6f94ff9d1b79eb9ff6b7e901c97ff03a89 Mon Sep 17 00:00:00 2001 From: Daman Arora Date: Tue, 6 Jan 2026 11:04:22 -0500 Subject: [PATCH 05/26] Add affinity group columns to kubernetes_cluster table --- .../src/main/resources/META-INF/db/schema-42210to42300.sql | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql b/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql index 07f394b19c90..a297b19fc1d2 100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql @@ -19,6 +19,12 @@ -- Schema upgrade from 4.22.1.0 to 4.23.0.0 --; + +-- Add affinity group columns to kubernetes_cluster table for CKS affinity group support +CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.kubernetes_cluster', 'control_node_affinity_group_id', 'BIGINT(20) UNSIGNED DEFAULT NULL COMMENT "affinity group id for control nodes"'); +CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.kubernetes_cluster', 'worker_node_affinity_group_id', 'BIGINT(20) UNSIGNED DEFAULT NULL COMMENT "affinity group id for worker nodes"'); +CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.kubernetes_cluster', 'etcd_node_affinity_group_id', 'BIGINT(20) UNSIGNED DEFAULT NULL COMMENT "affinity group id for etcd nodes"'); + -- Update value to random for the config 'vm.allocation.algorithm' or 'volume.allocation.algorithm' if configured as userconcentratedpod_random -- Update value to firstfit for the config 'vm.allocation.algorithm' or 'volume.allocation.algorithm' if configured as userconcentratedpod_firstfit UPDATE `cloud`.`configuration` SET value='random' WHERE name IN ('vm.allocation.algorithm', 'volume.allocation.algorithm') AND value='userconcentratedpod_random'; From 8bf7a453c9d12cba42435b2261aea278aa2b63ff Mon Sep 17 00:00:00 2001 From: Daman Arora Date: Tue, 6 Jan 2026 11:04:48 -0500 Subject: [PATCH 06/26] Add affinity group ID fields and accessors to KubernetesCluster and KubernetesClusterVO --- .../kubernetes/cluster/KubernetesCluster.java | 3 ++ .../cluster/KubernetesClusterVO.java | 33 +++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/api/src/main/java/com/cloud/kubernetes/cluster/KubernetesCluster.java b/api/src/main/java/com/cloud/kubernetes/cluster/KubernetesCluster.java index ce905b293ff3..4426fe011653 100644 --- a/api/src/main/java/com/cloud/kubernetes/cluster/KubernetesCluster.java +++ b/api/src/main/java/com/cloud/kubernetes/cluster/KubernetesCluster.java @@ -170,6 +170,9 @@ enum State { Long getWorkerNodeTemplateId(); Long getEtcdNodeTemplateId(); Long getEtcdNodeCount(); + Long getControlNodeAffinityGroupId(); + Long getWorkerNodeAffinityGroupId(); + Long getEtcdNodeAffinityGroupId(); Long getCniConfigId(); String getCniConfigDetails(); boolean isCsiEnabled(); diff --git a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/KubernetesClusterVO.java b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/KubernetesClusterVO.java index 7dfd0043e320..6d28dd7d2e5c 100644 --- a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/KubernetesClusterVO.java +++ b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/KubernetesClusterVO.java @@ -139,6 +139,15 @@ public class KubernetesClusterVO implements KubernetesCluster { @Column(name = "etcd_node_template_id") private Long etcdNodeTemplateId; + @Column(name = "control_node_affinity_group_id") + private Long controlNodeAffinityGroupId; + + @Column(name = "worker_node_affinity_group_id") + private Long workerNodeAffinityGroupId; + + @Column(name = "etcd_node_affinity_group_id") + private Long etcdNodeAffinityGroupId; + @Column(name = "cni_config_id", nullable = true) private Long cniConfigId = null; @@ -509,6 +518,30 @@ public void setControlNodeTemplateId(Long controlNodeTemplateId) { this.controlNodeTemplateId = controlNodeTemplateId; } + public Long getControlNodeAffinityGroupId() { + return controlNodeAffinityGroupId; + } + + public void setControlNodeAffinityGroupId(Long controlNodeAffinityGroupId) { + this.controlNodeAffinityGroupId = controlNodeAffinityGroupId; + } + + public Long getWorkerNodeAffinityGroupId() { + return workerNodeAffinityGroupId; + } + + public void setWorkerNodeAffinityGroupId(Long workerNodeAffinityGroupId) { + this.workerNodeAffinityGroupId = workerNodeAffinityGroupId; + } + + public Long getEtcdNodeAffinityGroupId() { + return etcdNodeAffinityGroupId; + } + + public void setEtcdNodeAffinityGroupId(Long etcdNodeAffinityGroupId) { + this.etcdNodeAffinityGroupId = etcdNodeAffinityGroupId; + } + public Long getCniConfigId() { return cniConfigId; } From 4706d0315e0d348da9f1cb137b7928c45b191154 Mon Sep 17 00:00:00 2001 From: Daman Arora Date: Tue, 6 Jan 2026 11:07:43 -0500 Subject: [PATCH 07/26] Add affinity group handling for worker, control, and etcd nodes in KubernetesClusterManagerImpl --- .../cluster/KubernetesClusterManagerImpl.java | 10 ++++++++++ 1 file changed, 10 insertions(+) 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 e6ed850fba58..66b491f2c315 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 @@ -1627,6 +1627,7 @@ public KubernetesCluster createManagedKubernetesCluster(CreateKubernetesClusterC } Map templateNodeTypeMap = cmd.getTemplateNodeTypeMap(); + Map affinityGroupNodeTypeMap = cmd.getAffinityGroupNodeTypeMap(); final VMTemplateVO finalTemplate = getKubernetesServiceTemplate(zone, hypervisorType, templateNodeTypeMap, DEFAULT, clusterKubernetesVersion); final VMTemplateVO controlNodeTemplate = getKubernetesServiceTemplate(zone, hypervisorType, templateNodeTypeMap, CONTROL, clusterKubernetesVersion); final VMTemplateVO workerNodeTemplate = getKubernetesServiceTemplate(zone, hypervisorType, templateNodeTypeMap, WORKER, clusterKubernetesVersion); @@ -1667,6 +1668,15 @@ public KubernetesClusterVO doInTransaction(TransactionStatus status) { } newCluster.setWorkerNodeTemplateId(workerNodeTemplate.getId()); newCluster.setControlNodeTemplateId(controlNodeTemplate.getId()); + if (affinityGroupNodeTypeMap.containsKey(WORKER.name())) { + newCluster.setWorkerNodeAffinityGroupId(affinityGroupNodeTypeMap.get(WORKER.name())); + } + if (affinityGroupNodeTypeMap.containsKey(CONTROL.name())) { + newCluster.setControlNodeAffinityGroupId(affinityGroupNodeTypeMap.get(CONTROL.name())); + } + if (etcdNodes > 0 && affinityGroupNodeTypeMap.containsKey(ETCD.name())) { + newCluster.setEtcdNodeAffinityGroupId(affinityGroupNodeTypeMap.get(ETCD.name())); + } if (zone.isSecurityGroupEnabled()) { newCluster.setSecurityGroupId(finalSecurityGroup.getId()); } From fe5c0260d635dde934cff10705474d837d188d65 Mon Sep 17 00:00:00 2001 From: Daman Arora Date: Tue, 6 Jan 2026 12:37:42 -0500 Subject: [PATCH 08/26] Refactor affinity group handling in KubernetesCluster and KubernetesClusterVO to support multiple IDs --- .../kubernetes/cluster/KubernetesCluster.java | 6 ++-- .../META-INF/db/schema-42210to42300.sql | 6 ++-- .../cluster/KubernetesClusterVO.java | 36 +++++++++---------- 3 files changed, 24 insertions(+), 24 deletions(-) diff --git a/api/src/main/java/com/cloud/kubernetes/cluster/KubernetesCluster.java b/api/src/main/java/com/cloud/kubernetes/cluster/KubernetesCluster.java index 4426fe011653..6d15f02246be 100644 --- a/api/src/main/java/com/cloud/kubernetes/cluster/KubernetesCluster.java +++ b/api/src/main/java/com/cloud/kubernetes/cluster/KubernetesCluster.java @@ -170,9 +170,9 @@ enum State { Long getWorkerNodeTemplateId(); Long getEtcdNodeTemplateId(); Long getEtcdNodeCount(); - Long getControlNodeAffinityGroupId(); - Long getWorkerNodeAffinityGroupId(); - Long getEtcdNodeAffinityGroupId(); + String getControlNodeAffinityGroupIds(); + String getWorkerNodeAffinityGroupIds(); + String getEtcdNodeAffinityGroupIds(); Long getCniConfigId(); String getCniConfigDetails(); boolean isCsiEnabled(); diff --git a/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql b/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql index a297b19fc1d2..6bfe2164203d 100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql @@ -21,9 +21,9 @@ -- Add affinity group columns to kubernetes_cluster table for CKS affinity group support -CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.kubernetes_cluster', 'control_node_affinity_group_id', 'BIGINT(20) UNSIGNED DEFAULT NULL COMMENT "affinity group id for control nodes"'); -CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.kubernetes_cluster', 'worker_node_affinity_group_id', 'BIGINT(20) UNSIGNED DEFAULT NULL COMMENT "affinity group id for worker nodes"'); -CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.kubernetes_cluster', 'etcd_node_affinity_group_id', 'BIGINT(20) UNSIGNED DEFAULT NULL COMMENT "affinity group id for etcd nodes"'); +CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.kubernetes_cluster', 'control_node_affinity_group_ids', 'VARCHAR(1024) DEFAULT NULL COMMENT "comma-separated affinity group UUIDs for control nodes"'); +CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.kubernetes_cluster', 'worker_node_affinity_group_ids', 'VARCHAR(1024) DEFAULT NULL COMMENT "comma-separated affinity group UUIDs for worker nodes"'); +CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.kubernetes_cluster', 'etcd_node_affinity_group_ids', 'VARCHAR(1024) DEFAULT NULL COMMENT "comma-separated affinity group UUIDs for etcd nodes"'); -- Update value to random for the config 'vm.allocation.algorithm' or 'volume.allocation.algorithm' if configured as userconcentratedpod_random -- Update value to firstfit for the config 'vm.allocation.algorithm' or 'volume.allocation.algorithm' if configured as userconcentratedpod_firstfit diff --git a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/KubernetesClusterVO.java b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/KubernetesClusterVO.java index 6d28dd7d2e5c..3075650ec452 100644 --- a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/KubernetesClusterVO.java +++ b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/KubernetesClusterVO.java @@ -139,14 +139,14 @@ public class KubernetesClusterVO implements KubernetesCluster { @Column(name = "etcd_node_template_id") private Long etcdNodeTemplateId; - @Column(name = "control_node_affinity_group_id") - private Long controlNodeAffinityGroupId; + @Column(name = "control_node_affinity_group_ids", length = 1024) + private String controlNodeAffinityGroupIds; - @Column(name = "worker_node_affinity_group_id") - private Long workerNodeAffinityGroupId; + @Column(name = "worker_node_affinity_group_ids", length = 1024) + private String workerNodeAffinityGroupIds; - @Column(name = "etcd_node_affinity_group_id") - private Long etcdNodeAffinityGroupId; + @Column(name = "etcd_node_affinity_group_ids", length = 1024) + private String etcdNodeAffinityGroupIds; @Column(name = "cni_config_id", nullable = true) private Long cniConfigId = null; @@ -518,28 +518,28 @@ public void setControlNodeTemplateId(Long controlNodeTemplateId) { this.controlNodeTemplateId = controlNodeTemplateId; } - public Long getControlNodeAffinityGroupId() { - return controlNodeAffinityGroupId; + public String getControlNodeAffinityGroupIds() { + return controlNodeAffinityGroupIds; } - public void setControlNodeAffinityGroupId(Long controlNodeAffinityGroupId) { - this.controlNodeAffinityGroupId = controlNodeAffinityGroupId; + public void setControlNodeAffinityGroupIds(String controlNodeAffinityGroupIds) { + this.controlNodeAffinityGroupIds = controlNodeAffinityGroupIds; } - public Long getWorkerNodeAffinityGroupId() { - return workerNodeAffinityGroupId; + public String getWorkerNodeAffinityGroupIds() { + return workerNodeAffinityGroupIds; } - public void setWorkerNodeAffinityGroupId(Long workerNodeAffinityGroupId) { - this.workerNodeAffinityGroupId = workerNodeAffinityGroupId; + public void setWorkerNodeAffinityGroupIds(String workerNodeAffinityGroupIds) { + this.workerNodeAffinityGroupIds = workerNodeAffinityGroupIds; } - public Long getEtcdNodeAffinityGroupId() { - return etcdNodeAffinityGroupId; + public String getEtcdNodeAffinityGroupIds() { + return etcdNodeAffinityGroupIds; } - public void setEtcdNodeAffinityGroupId(Long etcdNodeAffinityGroupId) { - this.etcdNodeAffinityGroupId = etcdNodeAffinityGroupId; + public void setEtcdNodeAffinityGroupIds(String etcdNodeAffinityGroupIds) { + this.etcdNodeAffinityGroupIds = etcdNodeAffinityGroupIds; } public Long getCniConfigId() { From 4da3bcec83c55d084e1840104e2e9d4ff0757b3f Mon Sep 17 00:00:00 2001 From: Daman Arora Date: Tue, 6 Jan 2026 12:38:23 -0500 Subject: [PATCH 09/26] Update affinity group handling to support multiple IDs in KubernetesServiceHelper and related classes --- .../cluster/KubernetesServiceHelper.java | 2 +- .../cluster/KubernetesClusterManagerImpl.java | 8 +-- .../cluster/KubernetesServiceHelperImpl.java | 54 +++++++++++++------ .../cluster/CreateKubernetesClusterCmd.java | 2 +- 4 files changed, 44 insertions(+), 22 deletions(-) diff --git a/api/src/main/java/com/cloud/kubernetes/cluster/KubernetesServiceHelper.java b/api/src/main/java/com/cloud/kubernetes/cluster/KubernetesServiceHelper.java index a9f76c174b59..1bda7019d1df 100644 --- a/api/src/main/java/com/cloud/kubernetes/cluster/KubernetesServiceHelper.java +++ b/api/src/main/java/com/cloud/kubernetes/cluster/KubernetesServiceHelper.java @@ -36,6 +36,6 @@ enum KubernetesClusterNodeType { boolean isValidNodeType(String nodeType); Map getServiceOfferingNodeTypeMap(Map> serviceOfferingNodeTypeMap); Map getTemplateNodeTypeMap(Map> templateNodeTypeMap); - Map getAffinityGroupNodeTypeMap(Map> affinityGroupNodeTypeMap); + Map getAffinityGroupNodeTypeMap(Map> affinityGroupNodeTypeMap); void cleanupForAccount(Account account); } 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 66b491f2c315..8699eeb35475 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 @@ -1627,7 +1627,7 @@ public KubernetesCluster createManagedKubernetesCluster(CreateKubernetesClusterC } Map templateNodeTypeMap = cmd.getTemplateNodeTypeMap(); - Map affinityGroupNodeTypeMap = cmd.getAffinityGroupNodeTypeMap(); + Map affinityGroupNodeTypeMap = cmd.getAffinityGroupNodeTypeMap(); final VMTemplateVO finalTemplate = getKubernetesServiceTemplate(zone, hypervisorType, templateNodeTypeMap, DEFAULT, clusterKubernetesVersion); final VMTemplateVO controlNodeTemplate = getKubernetesServiceTemplate(zone, hypervisorType, templateNodeTypeMap, CONTROL, clusterKubernetesVersion); final VMTemplateVO workerNodeTemplate = getKubernetesServiceTemplate(zone, hypervisorType, templateNodeTypeMap, WORKER, clusterKubernetesVersion); @@ -1669,13 +1669,13 @@ public KubernetesClusterVO doInTransaction(TransactionStatus status) { newCluster.setWorkerNodeTemplateId(workerNodeTemplate.getId()); newCluster.setControlNodeTemplateId(controlNodeTemplate.getId()); if (affinityGroupNodeTypeMap.containsKey(WORKER.name())) { - newCluster.setWorkerNodeAffinityGroupId(affinityGroupNodeTypeMap.get(WORKER.name())); + newCluster.setWorkerNodeAffinityGroupIds(affinityGroupNodeTypeMap.get(WORKER.name())); } if (affinityGroupNodeTypeMap.containsKey(CONTROL.name())) { - newCluster.setControlNodeAffinityGroupId(affinityGroupNodeTypeMap.get(CONTROL.name())); + newCluster.setControlNodeAffinityGroupIds(affinityGroupNodeTypeMap.get(CONTROL.name())); } if (etcdNodes > 0 && affinityGroupNodeTypeMap.containsKey(ETCD.name())) { - newCluster.setEtcdNodeAffinityGroupId(affinityGroupNodeTypeMap.get(ETCD.name())); + newCluster.setEtcdNodeAffinityGroupIds(affinityGroupNodeTypeMap.get(ETCD.name())); } if (zone.isSecurityGroupEnabled()) { newCluster.setSecurityGroupId(finalSecurityGroup.getId()); diff --git a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/KubernetesServiceHelperImpl.java b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/KubernetesServiceHelperImpl.java index 62e190b35bd4..85a4191ed20d 100644 --- a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/KubernetesServiceHelperImpl.java +++ b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/KubernetesServiceHelperImpl.java @@ -248,8 +248,8 @@ public Map getTemplateNodeTypeMap(Map> return mapping; } - protected void checkNodeTypeAffinityGroupEntryCompleteness(String nodeTypeStr, String affinityGroupUuid) { - if (StringUtils.isAnyBlank(nodeTypeStr, affinityGroupUuid)) { + protected void checkNodeTypeAffinityGroupEntryCompleteness(String nodeTypeStr, String affinityGroupUuids) { + if (StringUtils.isAnyBlank(nodeTypeStr, affinityGroupUuids)) { String error = String.format("Any Node Type to Affinity Group entry should have a valid '%s' and '%s' values", VmDetailConstants.CKS_NODE_TYPE, VmDetailConstants.AFFINITY_GROUP); logger.error(error); @@ -257,12 +257,21 @@ protected void checkNodeTypeAffinityGroupEntryCompleteness(String nodeTypeStr, S } } - protected void checkNodeTypeAffinityGroupEntryValues(String nodeTypeStr, AffinityGroup affinityGroup, String affinityGroupUuid) { + protected void checkNodeTypeAffinityGroupEntryNodeType(String nodeTypeStr) { if (!isValidNodeType(nodeTypeStr)) { String error = String.format("The provided value '%s' for Node Type is invalid", nodeTypeStr); logger.error(error); - throw new InvalidParameterValueException(String.format(error)); + throw new InvalidParameterValueException(error); + } + } + + protected void validateAffinityGroupUuid(String affinityGroupUuid) { + if (StringUtils.isBlank(affinityGroupUuid)) { + String error = "Empty affinity group UUID provided"; + logger.error(error); + throw new InvalidParameterValueException(error); } + AffinityGroup affinityGroup = affinityGroupDao.findByUuid(affinityGroupUuid); if (affinityGroup == null) { String error = String.format("Cannot find an affinity group with ID %s", affinityGroupUuid); logger.error(error); @@ -270,31 +279,44 @@ protected void checkNodeTypeAffinityGroupEntryValues(String nodeTypeStr, Affinit } } - protected void addNodeTypeAffinityGroupEntry(String nodeTypeStr, String affinityGroupUuid, AffinityGroup affinityGroup, Map mapping) { + protected String validateAndNormalizeAffinityGroupUuids(String affinityGroupUuids) { + String[] uuids = affinityGroupUuids.split(","); + StringBuilder normalizedUuids = new StringBuilder(); + for (int i = 0; i < uuids.length; i++) { + String uuid = uuids[i].trim(); + validateAffinityGroupUuid(uuid); + if (i > 0) { + normalizedUuids.append(","); + } + normalizedUuids.append(uuid); + } + return normalizedUuids.toString(); + } + + protected void addNodeTypeAffinityGroupEntry(String nodeTypeStr, String validatedAffinityGroupUuids, Map mapping) { if (logger.isDebugEnabled()) { - logger.debug(String.format("Node Type: '%s' should use affinity group ID: '%s'", nodeTypeStr, affinityGroupUuid)); + logger.debug(String.format("Node Type: '%s' should use affinity group IDs: '%s'", nodeTypeStr, validatedAffinityGroupUuids)); } KubernetesClusterNodeType nodeType = KubernetesClusterNodeType.valueOf(nodeTypeStr.toUpperCase()); - mapping.put(nodeType.name(), affinityGroup.getId()); + mapping.put(nodeType.name(), validatedAffinityGroupUuids); } - protected void processNodeTypeAffinityGroupEntryAndAddToMappingIfValid(Map entry, Map mapping) { + protected void processNodeTypeAffinityGroupEntryAndAddToMappingIfValid(Map entry, Map mapping) { if (MapUtils.isEmpty(entry)) { return; } String nodeTypeStr = entry.get(VmDetailConstants.CKS_NODE_TYPE); - String affinityGroupUuid = entry.get(VmDetailConstants.AFFINITY_GROUP); - checkNodeTypeAffinityGroupEntryCompleteness(nodeTypeStr, affinityGroupUuid); + String affinityGroupUuids = entry.get(VmDetailConstants.AFFINITY_GROUP); + checkNodeTypeAffinityGroupEntryCompleteness(nodeTypeStr, affinityGroupUuids); + checkNodeTypeAffinityGroupEntryNodeType(nodeTypeStr); - AffinityGroup affinityGroup = affinityGroupDao.findByUuid(affinityGroupUuid); - checkNodeTypeAffinityGroupEntryValues(nodeTypeStr, affinityGroup, affinityGroupUuid); - - addNodeTypeAffinityGroupEntry(nodeTypeStr, affinityGroupUuid, affinityGroup, mapping); + String validatedUuids = validateAndNormalizeAffinityGroupUuids(affinityGroupUuids); + addNodeTypeAffinityGroupEntry(nodeTypeStr, validatedUuids, mapping); } @Override - public Map getAffinityGroupNodeTypeMap(Map> affinityGroupNodeTypeMap) { - Map mapping = new HashMap<>(); + public Map getAffinityGroupNodeTypeMap(Map> affinityGroupNodeTypeMap) { + Map mapping = new HashMap<>(); if (MapUtils.isNotEmpty(affinityGroupNodeTypeMap)) { for (Map entry : affinityGroupNodeTypeMap.values()) { processNodeTypeAffinityGroupEntryAndAddToMappingIfValid(entry, mapping); diff --git a/plugins/integrations/kubernetes-service/src/main/java/org/apache/cloudstack/api/command/user/kubernetes/cluster/CreateKubernetesClusterCmd.java b/plugins/integrations/kubernetes-service/src/main/java/org/apache/cloudstack/api/command/user/kubernetes/cluster/CreateKubernetesClusterCmd.java index 4e458bbe6d72..1ab88cb13723 100644 --- a/plugins/integrations/kubernetes-service/src/main/java/org/apache/cloudstack/api/command/user/kubernetes/cluster/CreateKubernetesClusterCmd.java +++ b/plugins/integrations/kubernetes-service/src/main/java/org/apache/cloudstack/api/command/user/kubernetes/cluster/CreateKubernetesClusterCmd.java @@ -327,7 +327,7 @@ public Map getTemplateNodeTypeMap() { return kubernetesServiceHelper.getTemplateNodeTypeMap(templateNodeTypeMap); } - public Map getAffinityGroupNodeTypeMap() { + public Map getAffinityGroupNodeTypeMap() { return kubernetesServiceHelper.getAffinityGroupNodeTypeMap(affinityGroupNodeTypeMap); } From 58799c25ba8324e37edfc7257ecec7269b95e6ab Mon Sep 17 00:00:00 2001 From: Daman Arora Date: Tue, 6 Jan 2026 12:38:48 -0500 Subject: [PATCH 10/26] Refactor affinity group tests in KubernetesServiceHelperImplTest --- .../KubernetesServiceHelperImplTest.java | 133 ++++++++++++++---- 1 file changed, 109 insertions(+), 24 deletions(-) diff --git a/plugins/integrations/kubernetes-service/src/test/java/com/cloud/kubernetes/cluster/KubernetesServiceHelperImplTest.java b/plugins/integrations/kubernetes-service/src/test/java/com/cloud/kubernetes/cluster/KubernetesServiceHelperImplTest.java index 1ce8a99bd561..30596979d739 100644 --- a/plugins/integrations/kubernetes-service/src/test/java/com/cloud/kubernetes/cluster/KubernetesServiceHelperImplTest.java +++ b/plugins/integrations/kubernetes-service/src/test/java/com/cloud/kubernetes/cluster/KubernetesServiceHelperImplTest.java @@ -21,7 +21,6 @@ import java.util.Map; import java.util.UUID; -import org.apache.cloudstack.affinity.AffinityGroup; import org.apache.cloudstack.affinity.AffinityGroupVO; import org.apache.cloudstack.affinity.dao.AffinityGroupDao; import org.junit.Assert; @@ -209,35 +208,86 @@ public void testCheckNodeTypeAffinityGroupEntryCompletenessValid() { } @Test(expected = InvalidParameterValueException.class) - public void testCheckNodeTypeAffinityGroupEntryValuesInvalidNodeType() { - AffinityGroup affinityGroup = Mockito.mock(AffinityGroup.class); - kubernetesServiceHelper.checkNodeTypeAffinityGroupEntryValues("invalid-node-type", affinityGroup, "affinity-group-uuid"); + public void testCheckNodeTypeAffinityGroupEntryNodeTypeInvalid() { + kubernetesServiceHelper.checkNodeTypeAffinityGroupEntryNodeType("invalid-node-type"); + } + + @Test + public void testCheckNodeTypeAffinityGroupEntryNodeTypeValid() { + kubernetesServiceHelper.checkNodeTypeAffinityGroupEntryNodeType("control"); + } + + @Test(expected = InvalidParameterValueException.class) + public void testValidateAffinityGroupUuidBlank() { + kubernetesServiceHelper.validateAffinityGroupUuid(""); } @Test(expected = InvalidParameterValueException.class) - public void testCheckNodeTypeAffinityGroupEntryValuesNullAffinityGroup() { - kubernetesServiceHelper.checkNodeTypeAffinityGroupEntryValues("control", null, "affinity-group-uuid"); + public void testValidateAffinityGroupUuidNotFound() { + Mockito.when(affinityGroupDao.findByUuid("non-existent-uuid")).thenReturn(null); + kubernetesServiceHelper.validateAffinityGroupUuid("non-existent-uuid"); } @Test - public void testCheckNodeTypeAffinityGroupEntryValuesValid() { - AffinityGroup affinityGroup = Mockito.mock(AffinityGroup.class); - kubernetesServiceHelper.checkNodeTypeAffinityGroupEntryValues("control", affinityGroup, "affinity-group-uuid"); + public void testValidateAffinityGroupUuidValid() { + AffinityGroupVO affinityGroup = Mockito.mock(AffinityGroupVO.class); + Mockito.when(affinityGroupDao.findByUuid("valid-uuid")).thenReturn(affinityGroup); + kubernetesServiceHelper.validateAffinityGroupUuid("valid-uuid"); + } + + @Test + public void testValidateAndNormalizeAffinityGroupUuidsSingleUuid() { + AffinityGroupVO affinityGroup = Mockito.mock(AffinityGroupVO.class); + Mockito.when(affinityGroupDao.findByUuid("uuid1")).thenReturn(affinityGroup); + + String result = kubernetesServiceHelper.validateAndNormalizeAffinityGroupUuids("uuid1"); + Assert.assertEquals("uuid1", result); + } + + @Test + public void testValidateAndNormalizeAffinityGroupUuidsMultipleUuids() { + AffinityGroupVO affinityGroup1 = Mockito.mock(AffinityGroupVO.class); + AffinityGroupVO affinityGroup2 = Mockito.mock(AffinityGroupVO.class); + AffinityGroupVO affinityGroup3 = Mockito.mock(AffinityGroupVO.class); + Mockito.when(affinityGroupDao.findByUuid("uuid1")).thenReturn(affinityGroup1); + Mockito.when(affinityGroupDao.findByUuid("uuid2")).thenReturn(affinityGroup2); + Mockito.when(affinityGroupDao.findByUuid("uuid3")).thenReturn(affinityGroup3); + + String result = kubernetesServiceHelper.validateAndNormalizeAffinityGroupUuids("uuid1,uuid2,uuid3"); + Assert.assertEquals("uuid1,uuid2,uuid3", result); + } + + @Test + public void testValidateAndNormalizeAffinityGroupUuidsWithSpaces() { + AffinityGroupVO affinityGroup1 = Mockito.mock(AffinityGroupVO.class); + AffinityGroupVO affinityGroup2 = Mockito.mock(AffinityGroupVO.class); + Mockito.when(affinityGroupDao.findByUuid("uuid1")).thenReturn(affinityGroup1); + Mockito.when(affinityGroupDao.findByUuid("uuid2")).thenReturn(affinityGroup2); + + String result = kubernetesServiceHelper.validateAndNormalizeAffinityGroupUuids(" uuid1 , uuid2 "); + Assert.assertEquals("uuid1,uuid2", result); + } + + @Test(expected = InvalidParameterValueException.class) + public void testValidateAndNormalizeAffinityGroupUuidsOneInvalid() { + AffinityGroupVO affinityGroup1 = Mockito.mock(AffinityGroupVO.class); + Mockito.when(affinityGroupDao.findByUuid("uuid1")).thenReturn(affinityGroup1); + Mockito.when(affinityGroupDao.findByUuid("invalid-uuid")).thenReturn(null); + + kubernetesServiceHelper.validateAndNormalizeAffinityGroupUuids("uuid1,invalid-uuid"); } @Test public void testAddNodeTypeAffinityGroupEntry() { - AffinityGroup affinityGroup = Mockito.mock(AffinityGroup.class); - Mockito.when(affinityGroup.getId()).thenReturn(100L); - Map mapping = new HashMap<>(); - kubernetesServiceHelper.addNodeTypeAffinityGroupEntry("control", "affinity-group-uuid", affinityGroup, mapping); + Map mapping = new HashMap<>(); + kubernetesServiceHelper.addNodeTypeAffinityGroupEntry("control", "uuid1,uuid2", mapping); Assert.assertEquals(1, mapping.size()); - Assert.assertEquals(Long.valueOf(100L), mapping.get("CONTROL")); + Assert.assertEquals("uuid1,uuid2", mapping.get("CONTROL")); } @Test public void testProcessNodeTypeAffinityGroupEntryAndAddToMappingIfValidEmptyEntry() { - Map mapping = new HashMap<>(); + Map mapping = new HashMap<>(); kubernetesServiceHelper.processNodeTypeAffinityGroupEntryAndAddToMappingIfValid(new HashMap<>(), mapping); Assert.assertTrue(mapping.isEmpty()); } @@ -245,22 +295,38 @@ public void testProcessNodeTypeAffinityGroupEntryAndAddToMappingIfValidEmptyEntr @Test public void testProcessNodeTypeAffinityGroupEntryAndAddToMappingIfValidValidEntry() { AffinityGroupVO affinityGroup = Mockito.mock(AffinityGroupVO.class); - Mockito.when(affinityGroup.getId()).thenReturn(100L); Mockito.when(affinityGroupDao.findByUuid("affinity-group-uuid")).thenReturn(affinityGroup); Map entry = new HashMap<>(); entry.put(VmDetailConstants.CKS_NODE_TYPE, "control"); entry.put(VmDetailConstants.AFFINITY_GROUP, "affinity-group-uuid"); - Map mapping = new HashMap<>(); + Map mapping = new HashMap<>(); + kubernetesServiceHelper.processNodeTypeAffinityGroupEntryAndAddToMappingIfValid(entry, mapping); + Assert.assertEquals(1, mapping.size()); + Assert.assertEquals("affinity-group-uuid", mapping.get("CONTROL")); + } + + @Test + public void testProcessNodeTypeAffinityGroupEntryAndAddToMappingIfValidMultipleUuids() { + AffinityGroupVO affinityGroup1 = Mockito.mock(AffinityGroupVO.class); + AffinityGroupVO affinityGroup2 = Mockito.mock(AffinityGroupVO.class); + Mockito.when(affinityGroupDao.findByUuid("uuid1")).thenReturn(affinityGroup1); + Mockito.when(affinityGroupDao.findByUuid("uuid2")).thenReturn(affinityGroup2); + + Map entry = new HashMap<>(); + entry.put(VmDetailConstants.CKS_NODE_TYPE, "worker"); + entry.put(VmDetailConstants.AFFINITY_GROUP, "uuid1,uuid2"); + + Map mapping = new HashMap<>(); kubernetesServiceHelper.processNodeTypeAffinityGroupEntryAndAddToMappingIfValid(entry, mapping); Assert.assertEquals(1, mapping.size()); - Assert.assertEquals(Long.valueOf(100L), mapping.get("CONTROL")); + Assert.assertEquals("uuid1,uuid2", mapping.get("WORKER")); } @Test public void testGetAffinityGroupNodeTypeMapEmptyMap() { - Map result = kubernetesServiceHelper.getAffinityGroupNodeTypeMap(null); + Map result = kubernetesServiceHelper.getAffinityGroupNodeTypeMap(null); Assert.assertTrue(result.isEmpty()); result = kubernetesServiceHelper.getAffinityGroupNodeTypeMap(new HashMap<>()); @@ -270,11 +336,9 @@ public void testGetAffinityGroupNodeTypeMapEmptyMap() { @Test public void testGetAffinityGroupNodeTypeMapValidEntries() { AffinityGroupVO controlAffinityGroup = Mockito.mock(AffinityGroupVO.class); - Mockito.when(controlAffinityGroup.getId()).thenReturn(100L); Mockito.when(affinityGroupDao.findByUuid("control-affinity-uuid")).thenReturn(controlAffinityGroup); AffinityGroupVO workerAffinityGroup = Mockito.mock(AffinityGroupVO.class); - Mockito.when(workerAffinityGroup.getId()).thenReturn(200L); Mockito.when(affinityGroupDao.findByUuid("worker-affinity-uuid")).thenReturn(workerAffinityGroup); Map> affinityGroupNodeTypeMap = new HashMap<>(); @@ -289,9 +353,30 @@ public void testGetAffinityGroupNodeTypeMapValidEntries() { workerEntry.put(VmDetailConstants.AFFINITY_GROUP, "worker-affinity-uuid"); affinityGroupNodeTypeMap.put("1", workerEntry); - Map result = kubernetesServiceHelper.getAffinityGroupNodeTypeMap(affinityGroupNodeTypeMap); + Map result = kubernetesServiceHelper.getAffinityGroupNodeTypeMap(affinityGroupNodeTypeMap); Assert.assertEquals(2, result.size()); - Assert.assertEquals(Long.valueOf(100L), result.get("CONTROL")); - Assert.assertEquals(Long.valueOf(200L), result.get("WORKER")); + Assert.assertEquals("control-affinity-uuid", result.get("CONTROL")); + Assert.assertEquals("worker-affinity-uuid", result.get("WORKER")); + } + + @Test + public void testGetAffinityGroupNodeTypeMapMultipleUuidsPerNodeType() { + AffinityGroupVO ag1 = Mockito.mock(AffinityGroupVO.class); + AffinityGroupVO ag2 = Mockito.mock(AffinityGroupVO.class); + AffinityGroupVO ag3 = Mockito.mock(AffinityGroupVO.class); + Mockito.when(affinityGroupDao.findByUuid("ag1")).thenReturn(ag1); + Mockito.when(affinityGroupDao.findByUuid("ag2")).thenReturn(ag2); + Mockito.when(affinityGroupDao.findByUuid("ag3")).thenReturn(ag3); + + Map> affinityGroupNodeTypeMap = new HashMap<>(); + + Map controlEntry = new HashMap<>(); + controlEntry.put(VmDetailConstants.CKS_NODE_TYPE, "control"); + controlEntry.put(VmDetailConstants.AFFINITY_GROUP, "ag1,ag2,ag3"); + affinityGroupNodeTypeMap.put("0", controlEntry); + + Map result = kubernetesServiceHelper.getAffinityGroupNodeTypeMap(affinityGroupNodeTypeMap); + Assert.assertEquals(1, result.size()); + Assert.assertEquals("ag1,ag2,ag3", result.get("CONTROL")); } } From 0706410a3fb54c71ee88a20a27318ec8626f366a Mon Sep 17 00:00:00 2001 From: Daman Arora Date: Tue, 6 Jan 2026 15:23:30 -0500 Subject: [PATCH 11/26] Add per node type affinity group support for cks --- .../KubernetesClusterActionWorker.java | 37 +++++++++++++++++++ ...esClusterResourceModifierActionWorker.java | 9 ++--- .../KubernetesClusterStartWorker.java | 24 +++++------- 3 files changed, 49 insertions(+), 21 deletions(-) diff --git a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterActionWorker.java b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterActionWorker.java index 1d4b6e8d0a84..2c33d7cd3956 100644 --- a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterActionWorker.java +++ b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterActionWorker.java @@ -1112,4 +1112,41 @@ public Long getExplicitAffinityGroup(Long domainId, Long accountId) { } return null; } + + protected List getAffinityGroupIdsForNodeType(KubernetesClusterNodeType nodeType) { + String affinityGroupUuids = null; + switch (nodeType) { + case CONTROL: + affinityGroupUuids = kubernetesCluster.getControlNodeAffinityGroupIds(); + break; + case WORKER: + affinityGroupUuids = kubernetesCluster.getWorkerNodeAffinityGroupIds(); + break; + case ETCD: + affinityGroupUuids = kubernetesCluster.getEtcdNodeAffinityGroupIds(); + break; + default: + return new ArrayList<>(); + } + if (StringUtils.isBlank(affinityGroupUuids)) { + return new ArrayList<>(); + } + List affinityGroupIds = new ArrayList<>(); + for (String affinityGroupUuid : affinityGroupUuids.split(",")) { + AffinityGroupVO affinityGroupVO = affinityGroupDao.findByUuid(affinityGroupUuid.trim()); + if (affinityGroupVO != null) { + affinityGroupIds.add(affinityGroupVO.getId()); + } + } + return affinityGroupIds; + } + + protected List getMergedAffinityGroupIds(KubernetesClusterNodeType nodeType, Long domainId, Long accountId) { + List affinityGroupIds = getAffinityGroupIdsForNodeType(nodeType); + Long explicitAffinityGroupId = getExplicitAffinityGroup(domainId, accountId); + if (explicitAffinityGroupId != null && !affinityGroupIds.contains(explicitAffinityGroupId)) { + affinityGroupIds.add(explicitAffinityGroupId); + } + return affinityGroupIds.isEmpty() ? null : affinityGroupIds; + } } 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 cf69234d19e0..7442a1eccb46 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 @@ -26,7 +26,6 @@ import java.io.File; import java.io.IOException; import java.util.ArrayList; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -426,21 +425,19 @@ protected UserVm createKubernetesNode(String joinIp, Long domainId, Long account if (StringUtils.isNotBlank(kubernetesCluster.getKeyPair())) { keypairs.add(kubernetesCluster.getKeyPair()); } - Long affinityGroupId = getExplicitAffinityGroup(domainId, accountId); + List affinityGroupIds = getMergedAffinityGroupIds(WORKER, domainId, accountId); if (kubernetesCluster.getSecurityGroupId() != null && networkModel.checkSecurityGroupSupportForNetwork(owner, zone, networkIds, List.of(kubernetesCluster.getSecurityGroupId()))) { List securityGroupIds = new ArrayList<>(); securityGroupIds.add(kubernetesCluster.getSecurityGroupId()); nodeVm = userVmService.createAdvancedSecurityGroupVirtualMachine(zone, serviceOffering, workerNodeTemplate, networkIds, securityGroupIds, owner, hostName, hostName, null, null, null, null, Hypervisor.HypervisorType.None, BaseCmd.HTTPMethod.POST,base64UserData, null, null, keypairs, - null, addrs, null, null, Objects.nonNull(affinityGroupId) ? - Collections.singletonList(affinityGroupId) : null, customParameterMap, null, null, null, + null, addrs, null, null, affinityGroupIds, customParameterMap, null, null, null, null, true, null, UserVmManager.CKS_NODE, null, null); } else { nodeVm = userVmService.createAdvancedVirtualMachine(zone, serviceOffering, workerNodeTemplate, networkIds, owner, hostName, hostName, null, null, null, null, Hypervisor.HypervisorType.None, BaseCmd.HTTPMethod.POST, base64UserData, null, null, keypairs, - null, addrs, null, null, Objects.nonNull(affinityGroupId) ? - Collections.singletonList(affinityGroupId) : null, customParameterMap, null, null, null, null, true, UserVmManager.CKS_NODE, null, null, null); + null, addrs, null, null, affinityGroupIds, customParameterMap, null, null, null, null, true, UserVmManager.CKS_NODE, null, null, null); } if (logger.isInfoEnabled()) { logger.info("Created node VM : {}, {} in the Kubernetes cluster : {}", hostName, nodeVm, kubernetesCluster.getName()); diff --git a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterStartWorker.java b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterStartWorker.java index aa9317e619b0..4ed5ff0167c2 100644 --- a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterStartWorker.java +++ b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterStartWorker.java @@ -270,7 +270,7 @@ private Pair createKubernetesControlNode(final Network network, S keypairs.add(kubernetesCluster.getKeyPair()); } - Long affinityGroupId = getExplicitAffinityGroup(domainId, accountId); + List affinityGroupIds = getMergedAffinityGroupIds(CONTROL, domainId, accountId); String userDataDetails = kubernetesCluster.getCniConfigDetails(); if (kubernetesCluster.getSecurityGroupId() != null && networkModel.checkSecurityGroupSupportForNetwork(owner, zone, networkIds, @@ -279,15 +279,13 @@ private Pair createKubernetesControlNode(final Network network, S securityGroupIds.add(kubernetesCluster.getSecurityGroupId()); controlVm = userVmService.createAdvancedSecurityGroupVirtualMachine(zone, serviceOffering, controlNodeTemplate, networkIds, securityGroupIds, owner, hostName, hostName, null, null, null, null, Hypervisor.HypervisorType.None, BaseCmd.HTTPMethod.POST,base64UserData, userDataId, userDataDetails, keypairs, - requestedIps, addrs, null, null, Objects.nonNull(affinityGroupId) ? - Collections.singletonList(affinityGroupId) : null, customParameterMap, null, null, null, + requestedIps, addrs, null, null, affinityGroupIds, customParameterMap, null, null, null, null, true, null, UserVmManager.CKS_NODE, null, null); } else { controlVm = userVmService.createAdvancedVirtualMachine(zone, serviceOffering, controlNodeTemplate, networkIds, owner, hostName, hostName, null, null, null, null, Hypervisor.HypervisorType.None, BaseCmd.HTTPMethod.POST, base64UserData, userDataId, userDataDetails, keypairs, - requestedIps, addrs, null, null, Objects.nonNull(affinityGroupId) ? - Collections.singletonList(affinityGroupId) : null, customParameterMap, null, null, null, null, true, UserVmManager.CKS_NODE, null, null, null); + requestedIps, addrs, null, null, affinityGroupIds, customParameterMap, null, null, null, null, true, UserVmManager.CKS_NODE, null, null, null); } if (logger.isInfoEnabled()) { logger.info("Created control VM: {}, {} in the Kubernetes cluster: {}", controlVm, hostName, kubernetesCluster); @@ -439,7 +437,7 @@ private UserVm createKubernetesAdditionalControlNode(final String joinIp, final keypairs.add(kubernetesCluster.getKeyPair()); } - Long affinityGroupId = getExplicitAffinityGroup(domainId, accountId); + List affinityGroupIds = getMergedAffinityGroupIds(CONTROL, domainId, accountId); if (kubernetesCluster.getSecurityGroupId() != null && networkModel.checkSecurityGroupSupportForNetwork(owner, zone, networkIds, List.of(kubernetesCluster.getSecurityGroupId()))) { @@ -447,15 +445,13 @@ private UserVm createKubernetesAdditionalControlNode(final String joinIp, final securityGroupIds.add(kubernetesCluster.getSecurityGroupId()); additionalControlVm = userVmService.createAdvancedSecurityGroupVirtualMachine(zone, serviceOffering, controlNodeTemplate, networkIds, securityGroupIds, owner, hostName, hostName, null, null, null, null, Hypervisor.HypervisorType.None, BaseCmd.HTTPMethod.POST,base64UserData, null, null, keypairs, - null, addrs, null, null, Objects.nonNull(affinityGroupId) ? - Collections.singletonList(affinityGroupId) : null, customParameterMap, null, null, null, + null, addrs, null, null, affinityGroupIds, customParameterMap, null, null, null, null, true, null, UserVmManager.CKS_NODE, null, null); } else { additionalControlVm = userVmService.createAdvancedVirtualMachine(zone, serviceOffering, controlNodeTemplate, networkIds, owner, hostName, hostName, null, null, null, null, Hypervisor.HypervisorType.None, BaseCmd.HTTPMethod.POST, base64UserData, null, null, keypairs, - null, addrs, null, null, Objects.nonNull(affinityGroupId) ? - Collections.singletonList(affinityGroupId) : null, customParameterMap, null, null, null, null, true, UserVmManager.CKS_NODE, null, null, null); + null, addrs, null, null, affinityGroupIds, customParameterMap, null, null, null, null, true, UserVmManager.CKS_NODE, null, null, null); } if (logger.isInfoEnabled()) { @@ -483,7 +479,7 @@ private UserVm createEtcdNode(List requestedIps, List affinityGroupIds = getMergedAffinityGroupIds(ETCD, domainId, accountId); String hostName = etcdNodeHostnames.get(etcdNodeIndex); Map customParameterMap = new HashMap(); if (zone.isSecurityGroupEnabled()) { @@ -491,15 +487,13 @@ private UserVm createEtcdNode(List requestedIps, List Date: Wed, 7 Jan 2026 07:22:02 -0500 Subject: [PATCH 12/26] use a new table kubernetes_cluster_affinity_group_map instead of existing kubernetes_cluster --- .../kubernetes/cluster/KubernetesCluster.java | 3 -- .../META-INF/db/schema-42210to42300.sql | 18 +++++++--- .../cluster/KubernetesClusterVO.java | 33 ------------------- 3 files changed, 13 insertions(+), 41 deletions(-) diff --git a/api/src/main/java/com/cloud/kubernetes/cluster/KubernetesCluster.java b/api/src/main/java/com/cloud/kubernetes/cluster/KubernetesCluster.java index 6d15f02246be..ce905b293ff3 100644 --- a/api/src/main/java/com/cloud/kubernetes/cluster/KubernetesCluster.java +++ b/api/src/main/java/com/cloud/kubernetes/cluster/KubernetesCluster.java @@ -170,9 +170,6 @@ enum State { Long getWorkerNodeTemplateId(); Long getEtcdNodeTemplateId(); Long getEtcdNodeCount(); - String getControlNodeAffinityGroupIds(); - String getWorkerNodeAffinityGroupIds(); - String getEtcdNodeAffinityGroupIds(); Long getCniConfigId(); String getCniConfigDetails(); boolean isCsiEnabled(); diff --git a/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql b/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql index 6bfe2164203d..9048823e7a60 100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql @@ -20,16 +20,24 @@ --; --- Add affinity group columns to kubernetes_cluster table for CKS affinity group support -CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.kubernetes_cluster', 'control_node_affinity_group_ids', 'VARCHAR(1024) DEFAULT NULL COMMENT "comma-separated affinity group UUIDs for control nodes"'); -CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.kubernetes_cluster', 'worker_node_affinity_group_ids', 'VARCHAR(1024) DEFAULT NULL COMMENT "comma-separated affinity group UUIDs for worker nodes"'); -CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.kubernetes_cluster', 'etcd_node_affinity_group_ids', 'VARCHAR(1024) DEFAULT NULL COMMENT "comma-separated affinity group UUIDs for etcd nodes"'); - -- Update value to random for the config 'vm.allocation.algorithm' or 'volume.allocation.algorithm' if configured as userconcentratedpod_random -- Update value to firstfit for the config 'vm.allocation.algorithm' or 'volume.allocation.algorithm' if configured as userconcentratedpod_firstfit UPDATE `cloud`.`configuration` SET value='random' WHERE name IN ('vm.allocation.algorithm', 'volume.allocation.algorithm') AND value='userconcentratedpod_random'; UPDATE `cloud`.`configuration` SET value='firstfit' WHERE name IN ('vm.allocation.algorithm', 'volume.allocation.algorithm') AND value='userconcentratedpod_firstfit'; +-- Create kubernetes_cluster_affinity_group_map table for CKS per-node-type affinity groups +CREATE TABLE IF NOT EXISTS `cloud`.`kubernetes_cluster_affinity_group_map` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `cluster_id` bigint unsigned NOT NULL COMMENT 'kubernetes cluster id', + `node_type` varchar(32) NOT NULL COMMENT 'CONTROL, WORKER, or ETCD', + `affinity_group_id` bigint unsigned NOT NULL COMMENT 'affinity group id', + PRIMARY KEY (`id`), + CONSTRAINT `fk_kubernetes_cluster_ag_map__cluster_id` FOREIGN KEY (`cluster_id`) REFERENCES `kubernetes_cluster`(`id`) ON DELETE CASCADE, + CONSTRAINT `fk_kubernetes_cluster_ag_map__ag_id` FOREIGN KEY (`affinity_group_id`) REFERENCES `affinity_group`(`id`) ON DELETE CASCADE, + INDEX `i_kubernetes_cluster_ag_map__cluster_id`(`cluster_id`), + INDEX `i_kubernetes_cluster_ag_map__ag_id`(`affinity_group_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + -- Create webhook_filter table DROP TABLE IF EXISTS `cloud`.`webhook_filter`; CREATE TABLE IF NOT EXISTS `cloud`.`webhook_filter` ( diff --git a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/KubernetesClusterVO.java b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/KubernetesClusterVO.java index 3075650ec452..7dfd0043e320 100644 --- a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/KubernetesClusterVO.java +++ b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/KubernetesClusterVO.java @@ -139,15 +139,6 @@ public class KubernetesClusterVO implements KubernetesCluster { @Column(name = "etcd_node_template_id") private Long etcdNodeTemplateId; - @Column(name = "control_node_affinity_group_ids", length = 1024) - private String controlNodeAffinityGroupIds; - - @Column(name = "worker_node_affinity_group_ids", length = 1024) - private String workerNodeAffinityGroupIds; - - @Column(name = "etcd_node_affinity_group_ids", length = 1024) - private String etcdNodeAffinityGroupIds; - @Column(name = "cni_config_id", nullable = true) private Long cniConfigId = null; @@ -518,30 +509,6 @@ public void setControlNodeTemplateId(Long controlNodeTemplateId) { this.controlNodeTemplateId = controlNodeTemplateId; } - public String getControlNodeAffinityGroupIds() { - return controlNodeAffinityGroupIds; - } - - public void setControlNodeAffinityGroupIds(String controlNodeAffinityGroupIds) { - this.controlNodeAffinityGroupIds = controlNodeAffinityGroupIds; - } - - public String getWorkerNodeAffinityGroupIds() { - return workerNodeAffinityGroupIds; - } - - public void setWorkerNodeAffinityGroupIds(String workerNodeAffinityGroupIds) { - this.workerNodeAffinityGroupIds = workerNodeAffinityGroupIds; - } - - public String getEtcdNodeAffinityGroupIds() { - return etcdNodeAffinityGroupIds; - } - - public void setEtcdNodeAffinityGroupIds(String etcdNodeAffinityGroupIds) { - this.etcdNodeAffinityGroupIds = etcdNodeAffinityGroupIds; - } - public Long getCniConfigId() { return cniConfigId; } From 6e3ede9d78233f6d7c05c2545772eb11c6896d79 Mon Sep 17 00:00:00 2001 From: Daman Arora Date: Wed, 7 Jan 2026 08:39:09 -0500 Subject: [PATCH 13/26] add new resource KubernetesClusterAffinityGroupMap --- .../KubernetesClusterAffinityGroupMapVO.java | 83 +++++++++++++++++++ .../KubernetesClusterAffinityGroupMapDao.java | 29 +++++++ ...ernetesClusterAffinityGroupMapDaoImpl.java | 52 ++++++++++++ .../spring-kubernetes-service-context.xml | 1 + 4 files changed, 165 insertions(+) create mode 100644 plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/KubernetesClusterAffinityGroupMapVO.java create mode 100644 plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/dao/KubernetesClusterAffinityGroupMapDao.java create mode 100644 plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/dao/KubernetesClusterAffinityGroupMapDaoImpl.java diff --git a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/KubernetesClusterAffinityGroupMapVO.java b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/KubernetesClusterAffinityGroupMapVO.java new file mode 100644 index 000000000000..19babc86690d --- /dev/null +++ b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/KubernetesClusterAffinityGroupMapVO.java @@ -0,0 +1,83 @@ +// 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 javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; + +import org.apache.cloudstack.api.InternalIdentity; + +@Entity +@Table(name = "kubernetes_cluster_affinity_group_map") +public class KubernetesClusterAffinityGroupMapVO implements InternalIdentity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private Long id; + + @Column(name = "cluster_id") + private long clusterId; + + @Column(name = "node_type") + private String nodeType; + + @Column(name = "affinity_group_id") + private long affinityGroupId; + + public KubernetesClusterAffinityGroupMapVO() { + } + + public KubernetesClusterAffinityGroupMapVO(long clusterId, String nodeType, long affinityGroupId) { + this.clusterId = clusterId; + this.nodeType = nodeType; + this.affinityGroupId = affinityGroupId; + } + + @Override + public long getId() { + return id; + } + + public long getClusterId() { + return clusterId; + } + + public void setClusterId(long clusterId) { + this.clusterId = clusterId; + } + + public String getNodeType() { + return nodeType; + } + + public void setNodeType(String nodeType) { + this.nodeType = nodeType; + } + + public long getAffinityGroupId() { + return affinityGroupId; + } + + public void setAffinityGroupId(long affinityGroupId) { + this.affinityGroupId = affinityGroupId; + } +} diff --git a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/dao/KubernetesClusterAffinityGroupMapDao.java b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/dao/KubernetesClusterAffinityGroupMapDao.java new file mode 100644 index 000000000000..6e25dbbdf5a0 --- /dev/null +++ b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/dao/KubernetesClusterAffinityGroupMapDao.java @@ -0,0 +1,29 @@ +// 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.dao; + +import java.util.List; + +import com.cloud.kubernetes.cluster.KubernetesClusterAffinityGroupMapVO; +import com.cloud.utils.db.GenericDao; + +public interface KubernetesClusterAffinityGroupMapDao extends GenericDao { + + List listByClusterIdAndNodeType(long clusterId, String nodeType); + + List listAffinityGroupIdsByClusterIdAndNodeType(long clusterId, String nodeType); +} diff --git a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/dao/KubernetesClusterAffinityGroupMapDaoImpl.java b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/dao/KubernetesClusterAffinityGroupMapDaoImpl.java new file mode 100644 index 000000000000..9b802f33ccab --- /dev/null +++ b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/dao/KubernetesClusterAffinityGroupMapDaoImpl.java @@ -0,0 +1,52 @@ +// 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.dao; + +import java.util.List; +import java.util.stream.Collectors; + +import com.cloud.kubernetes.cluster.KubernetesClusterAffinityGroupMapVO; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; + +public class KubernetesClusterAffinityGroupMapDaoImpl extends GenericDaoBase + implements KubernetesClusterAffinityGroupMapDao { + + private final SearchBuilder clusterIdAndNodeTypeSearch; + + public KubernetesClusterAffinityGroupMapDaoImpl() { + clusterIdAndNodeTypeSearch = createSearchBuilder(); + clusterIdAndNodeTypeSearch.and("clusterId", clusterIdAndNodeTypeSearch.entity().getClusterId(), SearchCriteria.Op.EQ); + clusterIdAndNodeTypeSearch.and("nodeType", clusterIdAndNodeTypeSearch.entity().getNodeType(), SearchCriteria.Op.EQ); + clusterIdAndNodeTypeSearch.done(); + } + + @Override + public List listByClusterIdAndNodeType(long clusterId, String nodeType) { + SearchCriteria sc = clusterIdAndNodeTypeSearch.create(); + sc.setParameters("clusterId", clusterId); + sc.setParameters("nodeType", nodeType); + return listBy(sc); + } + + @Override + public List listAffinityGroupIdsByClusterIdAndNodeType(long clusterId, String nodeType) { + List maps = listByClusterIdAndNodeType(clusterId, nodeType); + return maps.stream().map(KubernetesClusterAffinityGroupMapVO::getAffinityGroupId).collect(Collectors.toList()); + } +} diff --git a/plugins/integrations/kubernetes-service/src/main/resources/META-INF/cloudstack/kubernetes-service/spring-kubernetes-service-context.xml b/plugins/integrations/kubernetes-service/src/main/resources/META-INF/cloudstack/kubernetes-service/spring-kubernetes-service-context.xml index 9d236eed26cd..053366786292 100644 --- a/plugins/integrations/kubernetes-service/src/main/resources/META-INF/cloudstack/kubernetes-service/spring-kubernetes-service-context.xml +++ b/plugins/integrations/kubernetes-service/src/main/resources/META-INF/cloudstack/kubernetes-service/spring-kubernetes-service-context.xml @@ -32,6 +32,7 @@ + From f625d6ed937600c0c38539413517c95afbb591a9 Mon Sep 17 00:00:00 2001 From: Daman Arora Date: Wed, 7 Jan 2026 08:41:17 -0500 Subject: [PATCH 14/26] Refactor affinity group mapping --- .../cluster/KubernetesServiceHelper.java | 3 +- .../cluster/KubernetesServiceHelperImpl.java | 67 +++++++-------- .../cluster/CreateKubernetesClusterCmd.java | 3 +- .../KubernetesServiceHelperImplTest.java | 85 ++++++++++++------- 4 files changed, 90 insertions(+), 68 deletions(-) diff --git a/api/src/main/java/com/cloud/kubernetes/cluster/KubernetesServiceHelper.java b/api/src/main/java/com/cloud/kubernetes/cluster/KubernetesServiceHelper.java index 1bda7019d1df..61bcd5368d83 100644 --- a/api/src/main/java/com/cloud/kubernetes/cluster/KubernetesServiceHelper.java +++ b/api/src/main/java/com/cloud/kubernetes/cluster/KubernetesServiceHelper.java @@ -18,6 +18,7 @@ import org.apache.cloudstack.acl.ControlledEntity; +import java.util.List; import java.util.Map; import com.cloud.user.Account; @@ -36,6 +37,6 @@ enum KubernetesClusterNodeType { boolean isValidNodeType(String nodeType); Map getServiceOfferingNodeTypeMap(Map> serviceOfferingNodeTypeMap); Map getTemplateNodeTypeMap(Map> templateNodeTypeMap); - Map getAffinityGroupNodeTypeMap(Map> affinityGroupNodeTypeMap); + Map> getAffinityGroupNodeTypeMap(Map> affinityGroupNodeTypeMap); void cleanupForAccount(Account account); } diff --git a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/KubernetesServiceHelperImpl.java b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/KubernetesServiceHelperImpl.java index 85a4191ed20d..62712514b2d0 100644 --- a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/KubernetesServiceHelperImpl.java +++ b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/KubernetesServiceHelperImpl.java @@ -18,7 +18,9 @@ import java.lang.reflect.Field; import java.lang.reflect.Modifier; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Objects; @@ -248,8 +250,8 @@ public Map getTemplateNodeTypeMap(Map> return mapping; } - protected void checkNodeTypeAffinityGroupEntryCompleteness(String nodeTypeStr, String affinityGroupUuids) { - if (StringUtils.isAnyBlank(nodeTypeStr, affinityGroupUuids)) { + protected void checkNodeTypeAffinityGroupEntryCompleteness(String nodeType, String affinityGroupUuids) { + if (StringUtils.isAnyBlank(nodeType, affinityGroupUuids)) { String error = String.format("Any Node Type to Affinity Group entry should have a valid '%s' and '%s' values", VmDetailConstants.CKS_NODE_TYPE, VmDetailConstants.AFFINITY_GROUP); logger.error(error); @@ -257,15 +259,15 @@ protected void checkNodeTypeAffinityGroupEntryCompleteness(String nodeTypeStr, S } } - protected void checkNodeTypeAffinityGroupEntryNodeType(String nodeTypeStr) { - if (!isValidNodeType(nodeTypeStr)) { - String error = String.format("The provided value '%s' for Node Type is invalid", nodeTypeStr); + protected void checkNodeTypeAffinityGroupEntryNodeType(String nodeType) { + if (!isValidNodeType(nodeType)) { + String error = String.format("The provided value '%s' for Node Type is invalid", nodeType); logger.error(error); throw new InvalidParameterValueException(error); } } - protected void validateAffinityGroupUuid(String affinityGroupUuid) { + protected Long validateAffinityGroupUuidAndGetId(String affinityGroupUuid) { if (StringUtils.isBlank(affinityGroupUuid)) { String error = "Empty affinity group UUID provided"; logger.error(error); @@ -277,55 +279,52 @@ protected void validateAffinityGroupUuid(String affinityGroupUuid) { logger.error(error); throw new InvalidParameterValueException(error); } + return affinityGroup.getId(); } - protected String validateAndNormalizeAffinityGroupUuids(String affinityGroupUuids) { + protected List validateAndGetAffinityGroupIds(String affinityGroupUuids) { String[] uuids = affinityGroupUuids.split(","); - StringBuilder normalizedUuids = new StringBuilder(); - for (int i = 0; i < uuids.length; i++) { - String uuid = uuids[i].trim(); - validateAffinityGroupUuid(uuid); - if (i > 0) { - normalizedUuids.append(","); - } - normalizedUuids.append(uuid); + List affinityGroupIds = new ArrayList<>(); + for (String uuid : uuids) { + String trimmedUuid = uuid.trim(); + Long affinityGroupId = validateAffinityGroupUuidAndGetId(trimmedUuid); + affinityGroupIds.add(affinityGroupId); } - return normalizedUuids.toString(); + return affinityGroupIds; } - protected void addNodeTypeAffinityGroupEntry(String nodeTypeStr, String validatedAffinityGroupUuids, Map mapping) { + protected void addNodeTypeAffinityGroupEntry(String nodeType, List affinityGroupIds, Map> nodeTypeToAffinityGroupIds) { if (logger.isDebugEnabled()) { - logger.debug(String.format("Node Type: '%s' should use affinity group IDs: '%s'", nodeTypeStr, validatedAffinityGroupUuids)); + logger.debug(String.format("Node Type: '%s' should use affinity group IDs: '%s'", nodeType, affinityGroupIds)); } - KubernetesClusterNodeType nodeType = KubernetesClusterNodeType.valueOf(nodeTypeStr.toUpperCase()); - mapping.put(nodeType.name(), validatedAffinityGroupUuids); + KubernetesClusterNodeType clusterNodeType = KubernetesClusterNodeType.valueOf(nodeType.toUpperCase()); + nodeTypeToAffinityGroupIds.put(clusterNodeType.name(), affinityGroupIds); } - protected void processNodeTypeAffinityGroupEntryAndAddToMappingIfValid(Map entry, Map mapping) { - if (MapUtils.isEmpty(entry)) { + protected void processNodeTypeAffinityGroupEntryAndAddToMappingIfValid(Map nodeTypeAffinityConfig, Map> nodeTypeToAffinityGroupIds) { + if (MapUtils.isEmpty(nodeTypeAffinityConfig)) { return; } - String nodeTypeStr = entry.get(VmDetailConstants.CKS_NODE_TYPE); - String affinityGroupUuids = entry.get(VmDetailConstants.AFFINITY_GROUP); - checkNodeTypeAffinityGroupEntryCompleteness(nodeTypeStr, affinityGroupUuids); - checkNodeTypeAffinityGroupEntryNodeType(nodeTypeStr); + String nodeType = nodeTypeAffinityConfig.get(VmDetailConstants.CKS_NODE_TYPE); + String affinityGroupUuids = nodeTypeAffinityConfig.get(VmDetailConstants.AFFINITY_GROUP); + checkNodeTypeAffinityGroupEntryCompleteness(nodeType, affinityGroupUuids); + checkNodeTypeAffinityGroupEntryNodeType(nodeType); - String validatedUuids = validateAndNormalizeAffinityGroupUuids(affinityGroupUuids); - addNodeTypeAffinityGroupEntry(nodeTypeStr, validatedUuids, mapping); + List affinityGroupIds = validateAndGetAffinityGroupIds(affinityGroupUuids); + addNodeTypeAffinityGroupEntry(nodeType, affinityGroupIds, nodeTypeToAffinityGroupIds); } @Override - public Map getAffinityGroupNodeTypeMap(Map> affinityGroupNodeTypeMap) { - Map mapping = new HashMap<>(); + public Map> getAffinityGroupNodeTypeMap(Map> affinityGroupNodeTypeMap) { + Map> nodeTypeToAffinityGroupIds = new HashMap<>(); if (MapUtils.isNotEmpty(affinityGroupNodeTypeMap)) { - for (Map entry : affinityGroupNodeTypeMap.values()) { - processNodeTypeAffinityGroupEntryAndAddToMappingIfValid(entry, mapping); + for (Map nodeTypeAffinityConfig : affinityGroupNodeTypeMap.values()) { + processNodeTypeAffinityGroupEntryAndAddToMappingIfValid(nodeTypeAffinityConfig, nodeTypeToAffinityGroupIds); } } - return mapping; + return nodeTypeToAffinityGroupIds; } - public void cleanupForAccount(Account account) { kubernetesClusterService.cleanupForAccount(account); } diff --git a/plugins/integrations/kubernetes-service/src/main/java/org/apache/cloudstack/api/command/user/kubernetes/cluster/CreateKubernetesClusterCmd.java b/plugins/integrations/kubernetes-service/src/main/java/org/apache/cloudstack/api/command/user/kubernetes/cluster/CreateKubernetesClusterCmd.java index 1ab88cb13723..bac3cd964865 100644 --- a/plugins/integrations/kubernetes-service/src/main/java/org/apache/cloudstack/api/command/user/kubernetes/cluster/CreateKubernetesClusterCmd.java +++ b/plugins/integrations/kubernetes-service/src/main/java/org/apache/cloudstack/api/command/user/kubernetes/cluster/CreateKubernetesClusterCmd.java @@ -17,6 +17,7 @@ package org.apache.cloudstack.api.command.user.kubernetes.cluster; import java.security.InvalidParameterException; +import java.util.List; import java.util.Map; import java.util.Objects; @@ -327,7 +328,7 @@ public Map getTemplateNodeTypeMap() { return kubernetesServiceHelper.getTemplateNodeTypeMap(templateNodeTypeMap); } - public Map getAffinityGroupNodeTypeMap() { + public Map> getAffinityGroupNodeTypeMap() { return kubernetesServiceHelper.getAffinityGroupNodeTypeMap(affinityGroupNodeTypeMap); } diff --git a/plugins/integrations/kubernetes-service/src/test/java/com/cloud/kubernetes/cluster/KubernetesServiceHelperImplTest.java b/plugins/integrations/kubernetes-service/src/test/java/com/cloud/kubernetes/cluster/KubernetesServiceHelperImplTest.java index 30596979d739..3994cadc307f 100644 --- a/plugins/integrations/kubernetes-service/src/test/java/com/cloud/kubernetes/cluster/KubernetesServiceHelperImplTest.java +++ b/plugins/integrations/kubernetes-service/src/test/java/com/cloud/kubernetes/cluster/KubernetesServiceHelperImplTest.java @@ -17,7 +17,9 @@ package com.cloud.kubernetes.cluster; +import java.util.Arrays; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.UUID; @@ -218,76 +220,87 @@ public void testCheckNodeTypeAffinityGroupEntryNodeTypeValid() { } @Test(expected = InvalidParameterValueException.class) - public void testValidateAffinityGroupUuidBlank() { - kubernetesServiceHelper.validateAffinityGroupUuid(""); + public void testValidateAffinityGroupUuidAndGetIdBlank() { + kubernetesServiceHelper.validateAffinityGroupUuidAndGetId(""); } @Test(expected = InvalidParameterValueException.class) - public void testValidateAffinityGroupUuidNotFound() { + public void testValidateAffinityGroupUuidAndGetIdNotFound() { Mockito.when(affinityGroupDao.findByUuid("non-existent-uuid")).thenReturn(null); - kubernetesServiceHelper.validateAffinityGroupUuid("non-existent-uuid"); + kubernetesServiceHelper.validateAffinityGroupUuidAndGetId("non-existent-uuid"); } @Test - public void testValidateAffinityGroupUuidValid() { + public void testValidateAffinityGroupUuidAndGetIdValid() { AffinityGroupVO affinityGroup = Mockito.mock(AffinityGroupVO.class); + Mockito.when(affinityGroup.getId()).thenReturn(100L); Mockito.when(affinityGroupDao.findByUuid("valid-uuid")).thenReturn(affinityGroup); - kubernetesServiceHelper.validateAffinityGroupUuid("valid-uuid"); + Long result = kubernetesServiceHelper.validateAffinityGroupUuidAndGetId("valid-uuid"); + Assert.assertEquals(Long.valueOf(100L), result); } @Test - public void testValidateAndNormalizeAffinityGroupUuidsSingleUuid() { + public void testValidateAndGetAffinityGroupIdsSingleUuid() { AffinityGroupVO affinityGroup = Mockito.mock(AffinityGroupVO.class); + Mockito.when(affinityGroup.getId()).thenReturn(1L); Mockito.when(affinityGroupDao.findByUuid("uuid1")).thenReturn(affinityGroup); - String result = kubernetesServiceHelper.validateAndNormalizeAffinityGroupUuids("uuid1"); - Assert.assertEquals("uuid1", result); + List result = kubernetesServiceHelper.validateAndGetAffinityGroupIds("uuid1"); + Assert.assertEquals(1, result.size()); + Assert.assertEquals(Long.valueOf(1L), result.get(0)); } @Test - public void testValidateAndNormalizeAffinityGroupUuidsMultipleUuids() { + public void testValidateAndGetAffinityGroupIdsMultipleUuids() { AffinityGroupVO affinityGroup1 = Mockito.mock(AffinityGroupVO.class); AffinityGroupVO affinityGroup2 = Mockito.mock(AffinityGroupVO.class); AffinityGroupVO affinityGroup3 = Mockito.mock(AffinityGroupVO.class); + Mockito.when(affinityGroup1.getId()).thenReturn(1L); + Mockito.when(affinityGroup2.getId()).thenReturn(2L); + Mockito.when(affinityGroup3.getId()).thenReturn(3L); Mockito.when(affinityGroupDao.findByUuid("uuid1")).thenReturn(affinityGroup1); Mockito.when(affinityGroupDao.findByUuid("uuid2")).thenReturn(affinityGroup2); Mockito.when(affinityGroupDao.findByUuid("uuid3")).thenReturn(affinityGroup3); - String result = kubernetesServiceHelper.validateAndNormalizeAffinityGroupUuids("uuid1,uuid2,uuid3"); - Assert.assertEquals("uuid1,uuid2,uuid3", result); + List result = kubernetesServiceHelper.validateAndGetAffinityGroupIds("uuid1,uuid2,uuid3"); + Assert.assertEquals(3, result.size()); + Assert.assertEquals(Arrays.asList(1L, 2L, 3L), result); } @Test - public void testValidateAndNormalizeAffinityGroupUuidsWithSpaces() { + public void testValidateAndGetAffinityGroupIdsWithSpaces() { AffinityGroupVO affinityGroup1 = Mockito.mock(AffinityGroupVO.class); AffinityGroupVO affinityGroup2 = Mockito.mock(AffinityGroupVO.class); + Mockito.when(affinityGroup1.getId()).thenReturn(1L); + Mockito.when(affinityGroup2.getId()).thenReturn(2L); Mockito.when(affinityGroupDao.findByUuid("uuid1")).thenReturn(affinityGroup1); Mockito.when(affinityGroupDao.findByUuid("uuid2")).thenReturn(affinityGroup2); - String result = kubernetesServiceHelper.validateAndNormalizeAffinityGroupUuids(" uuid1 , uuid2 "); - Assert.assertEquals("uuid1,uuid2", result); + List result = kubernetesServiceHelper.validateAndGetAffinityGroupIds(" uuid1 , uuid2 "); + Assert.assertEquals(2, result.size()); + Assert.assertEquals(Arrays.asList(1L, 2L), result); } @Test(expected = InvalidParameterValueException.class) - public void testValidateAndNormalizeAffinityGroupUuidsOneInvalid() { + public void testValidateAndGetAffinityGroupIdsOneInvalid() { AffinityGroupVO affinityGroup1 = Mockito.mock(AffinityGroupVO.class); Mockito.when(affinityGroupDao.findByUuid("uuid1")).thenReturn(affinityGroup1); Mockito.when(affinityGroupDao.findByUuid("invalid-uuid")).thenReturn(null); - kubernetesServiceHelper.validateAndNormalizeAffinityGroupUuids("uuid1,invalid-uuid"); + kubernetesServiceHelper.validateAndGetAffinityGroupIds("uuid1,invalid-uuid"); } @Test public void testAddNodeTypeAffinityGroupEntry() { - Map mapping = new HashMap<>(); - kubernetesServiceHelper.addNodeTypeAffinityGroupEntry("control", "uuid1,uuid2", mapping); + Map> mapping = new HashMap<>(); + kubernetesServiceHelper.addNodeTypeAffinityGroupEntry("control", Arrays.asList(1L, 2L), mapping); Assert.assertEquals(1, mapping.size()); - Assert.assertEquals("uuid1,uuid2", mapping.get("CONTROL")); + Assert.assertEquals(Arrays.asList(1L, 2L), mapping.get("CONTROL")); } @Test public void testProcessNodeTypeAffinityGroupEntryAndAddToMappingIfValidEmptyEntry() { - Map mapping = new HashMap<>(); + Map> mapping = new HashMap<>(); kubernetesServiceHelper.processNodeTypeAffinityGroupEntryAndAddToMappingIfValid(new HashMap<>(), mapping); Assert.assertTrue(mapping.isEmpty()); } @@ -295,22 +308,25 @@ public void testProcessNodeTypeAffinityGroupEntryAndAddToMappingIfValidEmptyEntr @Test public void testProcessNodeTypeAffinityGroupEntryAndAddToMappingIfValidValidEntry() { AffinityGroupVO affinityGroup = Mockito.mock(AffinityGroupVO.class); + Mockito.when(affinityGroup.getId()).thenReturn(100L); Mockito.when(affinityGroupDao.findByUuid("affinity-group-uuid")).thenReturn(affinityGroup); Map entry = new HashMap<>(); entry.put(VmDetailConstants.CKS_NODE_TYPE, "control"); entry.put(VmDetailConstants.AFFINITY_GROUP, "affinity-group-uuid"); - Map mapping = new HashMap<>(); + Map> mapping = new HashMap<>(); kubernetesServiceHelper.processNodeTypeAffinityGroupEntryAndAddToMappingIfValid(entry, mapping); Assert.assertEquals(1, mapping.size()); - Assert.assertEquals("affinity-group-uuid", mapping.get("CONTROL")); + Assert.assertEquals(Arrays.asList(100L), mapping.get("CONTROL")); } @Test public void testProcessNodeTypeAffinityGroupEntryAndAddToMappingIfValidMultipleUuids() { AffinityGroupVO affinityGroup1 = Mockito.mock(AffinityGroupVO.class); AffinityGroupVO affinityGroup2 = Mockito.mock(AffinityGroupVO.class); + Mockito.when(affinityGroup1.getId()).thenReturn(1L); + Mockito.when(affinityGroup2.getId()).thenReturn(2L); Mockito.when(affinityGroupDao.findByUuid("uuid1")).thenReturn(affinityGroup1); Mockito.when(affinityGroupDao.findByUuid("uuid2")).thenReturn(affinityGroup2); @@ -318,15 +334,15 @@ public void testProcessNodeTypeAffinityGroupEntryAndAddToMappingIfValidMultipleU entry.put(VmDetailConstants.CKS_NODE_TYPE, "worker"); entry.put(VmDetailConstants.AFFINITY_GROUP, "uuid1,uuid2"); - Map mapping = new HashMap<>(); + Map> mapping = new HashMap<>(); kubernetesServiceHelper.processNodeTypeAffinityGroupEntryAndAddToMappingIfValid(entry, mapping); Assert.assertEquals(1, mapping.size()); - Assert.assertEquals("uuid1,uuid2", mapping.get("WORKER")); + Assert.assertEquals(Arrays.asList(1L, 2L), mapping.get("WORKER")); } @Test public void testGetAffinityGroupNodeTypeMapEmptyMap() { - Map result = kubernetesServiceHelper.getAffinityGroupNodeTypeMap(null); + Map> result = kubernetesServiceHelper.getAffinityGroupNodeTypeMap(null); Assert.assertTrue(result.isEmpty()); result = kubernetesServiceHelper.getAffinityGroupNodeTypeMap(new HashMap<>()); @@ -336,9 +352,11 @@ public void testGetAffinityGroupNodeTypeMapEmptyMap() { @Test public void testGetAffinityGroupNodeTypeMapValidEntries() { AffinityGroupVO controlAffinityGroup = Mockito.mock(AffinityGroupVO.class); + Mockito.when(controlAffinityGroup.getId()).thenReturn(100L); Mockito.when(affinityGroupDao.findByUuid("control-affinity-uuid")).thenReturn(controlAffinityGroup); AffinityGroupVO workerAffinityGroup = Mockito.mock(AffinityGroupVO.class); + Mockito.when(workerAffinityGroup.getId()).thenReturn(200L); Mockito.when(affinityGroupDao.findByUuid("worker-affinity-uuid")).thenReturn(workerAffinityGroup); Map> affinityGroupNodeTypeMap = new HashMap<>(); @@ -353,17 +371,20 @@ public void testGetAffinityGroupNodeTypeMapValidEntries() { workerEntry.put(VmDetailConstants.AFFINITY_GROUP, "worker-affinity-uuid"); affinityGroupNodeTypeMap.put("1", workerEntry); - Map result = kubernetesServiceHelper.getAffinityGroupNodeTypeMap(affinityGroupNodeTypeMap); + Map> result = kubernetesServiceHelper.getAffinityGroupNodeTypeMap(affinityGroupNodeTypeMap); Assert.assertEquals(2, result.size()); - Assert.assertEquals("control-affinity-uuid", result.get("CONTROL")); - Assert.assertEquals("worker-affinity-uuid", result.get("WORKER")); + Assert.assertEquals(Arrays.asList(100L), result.get("CONTROL")); + Assert.assertEquals(Arrays.asList(200L), result.get("WORKER")); } @Test - public void testGetAffinityGroupNodeTypeMapMultipleUuidsPerNodeType() { + public void testGetAffinityGroupNodeTypeMapMultipleIdsPerNodeType() { AffinityGroupVO ag1 = Mockito.mock(AffinityGroupVO.class); AffinityGroupVO ag2 = Mockito.mock(AffinityGroupVO.class); AffinityGroupVO ag3 = Mockito.mock(AffinityGroupVO.class); + Mockito.when(ag1.getId()).thenReturn(1L); + Mockito.when(ag2.getId()).thenReturn(2L); + Mockito.when(ag3.getId()).thenReturn(3L); Mockito.when(affinityGroupDao.findByUuid("ag1")).thenReturn(ag1); Mockito.when(affinityGroupDao.findByUuid("ag2")).thenReturn(ag2); Mockito.when(affinityGroupDao.findByUuid("ag3")).thenReturn(ag3); @@ -375,8 +396,8 @@ public void testGetAffinityGroupNodeTypeMapMultipleUuidsPerNodeType() { controlEntry.put(VmDetailConstants.AFFINITY_GROUP, "ag1,ag2,ag3"); affinityGroupNodeTypeMap.put("0", controlEntry); - Map result = kubernetesServiceHelper.getAffinityGroupNodeTypeMap(affinityGroupNodeTypeMap); + Map> result = kubernetesServiceHelper.getAffinityGroupNodeTypeMap(affinityGroupNodeTypeMap); Assert.assertEquals(1, result.size()); - Assert.assertEquals("ag1,ag2,ag3", result.get("CONTROL")); + Assert.assertEquals(Arrays.asList(1L, 2L, 3L), result.get("CONTROL")); } } From 35a7bab9ca9505eb413070980a52390d14d00501 Mon Sep 17 00:00:00 2001 From: Daman Arora Date: Wed, 7 Jan 2026 08:41:46 -0500 Subject: [PATCH 15/26] use updated getAffinityGroupNodeTypeMap --- .../cluster/KubernetesClusterManagerImpl.java | 29 ++++++++++++------- 1 file changed, 19 insertions(+), 10 deletions(-) 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 8699eeb35475..92518c43a61d 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 @@ -169,6 +169,7 @@ import com.cloud.kubernetes.cluster.actionworkers.KubernetesClusterStartWorker; import com.cloud.kubernetes.cluster.actionworkers.KubernetesClusterStopWorker; import com.cloud.kubernetes.cluster.actionworkers.KubernetesClusterUpgradeWorker; +import com.cloud.kubernetes.cluster.dao.KubernetesClusterAffinityGroupMapDao; import com.cloud.kubernetes.cluster.dao.KubernetesClusterDao; import com.cloud.kubernetes.cluster.dao.KubernetesClusterDetailsDao; import com.cloud.kubernetes.cluster.dao.KubernetesClusterVmMapDao; @@ -315,6 +316,8 @@ public class KubernetesClusterManagerImpl extends ManagerBase implements Kuberne @Inject public KubernetesClusterDetailsDao kubernetesClusterDetailsDao; @Inject + public KubernetesClusterAffinityGroupMapDao kubernetesClusterAffinityGroupMapDao; + @Inject public KubernetesSupportedVersionDao kubernetesSupportedVersionDao; @Inject protected SSHKeyPairDao sshKeyPairDao; @@ -1187,6 +1190,20 @@ private Network getKubernetesClusterNetworkIfMissing(final String clusterName, f return network; } + private void persistAffinityGroupMappings(long clusterId, Map> affinityGroupNodeTypeMap) { + if (MapUtils.isEmpty(affinityGroupNodeTypeMap)) { + return; + } + for (Map.Entry> nodeTypeAffinityGroupEntry : affinityGroupNodeTypeMap.entrySet()) { + String nodeType = nodeTypeAffinityGroupEntry.getKey(); + List affinityGroupIds = nodeTypeAffinityGroupEntry.getValue(); + for (Long affinityGroupId : affinityGroupIds) { + kubernetesClusterAffinityGroupMapDao.persist( + new KubernetesClusterAffinityGroupMapVO(clusterId, nodeType, affinityGroupId)); + } + } + } + private void addKubernetesClusterDetails(final KubernetesCluster kubernetesCluster, final Network network, final CreateKubernetesClusterCmd cmd) { final String externalLoadBalancerIpAddress = cmd.getExternalLoadBalancerIpAddress(); final String dockerRegistryUserName = cmd.getDockerRegistryUserName(); @@ -1627,7 +1644,7 @@ public KubernetesCluster createManagedKubernetesCluster(CreateKubernetesClusterC } Map templateNodeTypeMap = cmd.getTemplateNodeTypeMap(); - Map affinityGroupNodeTypeMap = cmd.getAffinityGroupNodeTypeMap(); + Map> affinityGroupNodeTypeMap = cmd.getAffinityGroupNodeTypeMap(); final VMTemplateVO finalTemplate = getKubernetesServiceTemplate(zone, hypervisorType, templateNodeTypeMap, DEFAULT, clusterKubernetesVersion); final VMTemplateVO controlNodeTemplate = getKubernetesServiceTemplate(zone, hypervisorType, templateNodeTypeMap, CONTROL, clusterKubernetesVersion); final VMTemplateVO workerNodeTemplate = getKubernetesServiceTemplate(zone, hypervisorType, templateNodeTypeMap, WORKER, clusterKubernetesVersion); @@ -1668,20 +1685,12 @@ public KubernetesClusterVO doInTransaction(TransactionStatus status) { } newCluster.setWorkerNodeTemplateId(workerNodeTemplate.getId()); newCluster.setControlNodeTemplateId(controlNodeTemplate.getId()); - if (affinityGroupNodeTypeMap.containsKey(WORKER.name())) { - newCluster.setWorkerNodeAffinityGroupIds(affinityGroupNodeTypeMap.get(WORKER.name())); - } - if (affinityGroupNodeTypeMap.containsKey(CONTROL.name())) { - newCluster.setControlNodeAffinityGroupIds(affinityGroupNodeTypeMap.get(CONTROL.name())); - } - if (etcdNodes > 0 && affinityGroupNodeTypeMap.containsKey(ETCD.name())) { - newCluster.setEtcdNodeAffinityGroupIds(affinityGroupNodeTypeMap.get(ETCD.name())); - } if (zone.isSecurityGroupEnabled()) { newCluster.setSecurityGroupId(finalSecurityGroup.getId()); } newCluster.setCsiEnabled(cmd.getEnableCsi()); kubernetesClusterDao.persist(newCluster); + persistAffinityGroupMappings(newCluster.getId(), affinityGroupNodeTypeMap); addKubernetesClusterDetails(newCluster, defaultNetwork, cmd); return newCluster; } From af97ea391114f3026ad691ce8ebb1714254a52b7 Mon Sep 17 00:00:00 2001 From: Daman Arora Date: Wed, 7 Jan 2026 08:45:26 -0500 Subject: [PATCH 16/26] use DAO query instead of parsing comma-separated UUIDs --- .../KubernetesClusterActionWorker.java | 30 ++++--------------- 1 file changed, 5 insertions(+), 25 deletions(-) diff --git a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterActionWorker.java b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterActionWorker.java index 2c33d7cd3956..3e90cbd25d2e 100644 --- a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterActionWorker.java +++ b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterActionWorker.java @@ -90,6 +90,7 @@ import com.cloud.kubernetes.cluster.KubernetesClusterManagerImpl; import com.cloud.kubernetes.cluster.KubernetesClusterVO; import com.cloud.kubernetes.cluster.KubernetesClusterVmMapVO; +import com.cloud.kubernetes.cluster.dao.KubernetesClusterAffinityGroupMapDao; import com.cloud.kubernetes.cluster.dao.KubernetesClusterDao; import com.cloud.kubernetes.cluster.dao.KubernetesClusterDetailsDao; import com.cloud.kubernetes.cluster.dao.KubernetesClusterVmMapDao; @@ -217,6 +218,7 @@ public class KubernetesClusterActionWorker { protected KubernetesClusterDao kubernetesClusterDao; protected KubernetesClusterVmMapDao kubernetesClusterVmMapDao; protected KubernetesClusterDetailsDao kubernetesClusterDetailsDao; + protected KubernetesClusterAffinityGroupMapDao kubernetesClusterAffinityGroupMapDao; protected KubernetesSupportedVersionDao kubernetesSupportedVersionDao; protected KubernetesCluster kubernetesCluster; @@ -251,6 +253,7 @@ protected KubernetesClusterActionWorker(final KubernetesCluster kubernetesCluste this.kubernetesClusterDao = clusterManager.kubernetesClusterDao; this.kubernetesClusterDetailsDao = clusterManager.kubernetesClusterDetailsDao; this.kubernetesClusterVmMapDao = clusterManager.kubernetesClusterVmMapDao; + this.kubernetesClusterAffinityGroupMapDao = clusterManager.kubernetesClusterAffinityGroupMapDao; this.kubernetesSupportedVersionDao = clusterManager.kubernetesSupportedVersionDao; this.manager = clusterManager; } @@ -1114,31 +1117,8 @@ public Long getExplicitAffinityGroup(Long domainId, Long accountId) { } protected List getAffinityGroupIdsForNodeType(KubernetesClusterNodeType nodeType) { - String affinityGroupUuids = null; - switch (nodeType) { - case CONTROL: - affinityGroupUuids = kubernetesCluster.getControlNodeAffinityGroupIds(); - break; - case WORKER: - affinityGroupUuids = kubernetesCluster.getWorkerNodeAffinityGroupIds(); - break; - case ETCD: - affinityGroupUuids = kubernetesCluster.getEtcdNodeAffinityGroupIds(); - break; - default: - return new ArrayList<>(); - } - if (StringUtils.isBlank(affinityGroupUuids)) { - return new ArrayList<>(); - } - List affinityGroupIds = new ArrayList<>(); - for (String affinityGroupUuid : affinityGroupUuids.split(",")) { - AffinityGroupVO affinityGroupVO = affinityGroupDao.findByUuid(affinityGroupUuid.trim()); - if (affinityGroupVO != null) { - affinityGroupIds.add(affinityGroupVO.getId()); - } - } - return affinityGroupIds; + return new ArrayList<>(kubernetesClusterAffinityGroupMapDao.listAffinityGroupIdsByClusterIdAndNodeType( + kubernetesCluster.getId(), nodeType.name())); } protected List getMergedAffinityGroupIds(KubernetesClusterNodeType nodeType, Long domainId, Long accountId) { From c58dee04d72807c616d497cc1c28176746e55900 Mon Sep 17 00:00:00 2001 From: Daman Arora Date: Wed, 7 Jan 2026 09:15:46 -0500 Subject: [PATCH 17/26] remove affinity group mappings when a cluster is deleted --- .../cluster/KubernetesClusterManagerImpl.java | 1 + .../dao/KubernetesClusterAffinityGroupMapDao.java | 2 ++ .../KubernetesClusterAffinityGroupMapDaoImpl.java | 12 ++++++++++++ 3 files changed, 15 insertions(+) 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 92518c43a61d..75e764c52fa7 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 @@ -2068,6 +2068,7 @@ public boolean deleteKubernetesCluster(DeleteKubernetesClusterCmd cmd) throws Cl return Transaction.execute((TransactionCallback) status -> { kubernetesClusterDetailsDao.removeDetails(kubernetesClusterId); kubernetesClusterVmMapDao.removeByClusterId(kubernetesClusterId); + kubernetesClusterAffinityGroupMapDao.removeByClusterId(kubernetesClusterId); if (kubernetesClusterDao.remove(kubernetesClusterId)) { deleteProjectKubernetesAccountIfNeeded(cluster); return true; diff --git a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/dao/KubernetesClusterAffinityGroupMapDao.java b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/dao/KubernetesClusterAffinityGroupMapDao.java index 6e25dbbdf5a0..8c152153a2cd 100644 --- a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/dao/KubernetesClusterAffinityGroupMapDao.java +++ b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/dao/KubernetesClusterAffinityGroupMapDao.java @@ -26,4 +26,6 @@ public interface KubernetesClusterAffinityGroupMapDao extends GenericDao listByClusterIdAndNodeType(long clusterId, String nodeType); List listAffinityGroupIdsByClusterIdAndNodeType(long clusterId, String nodeType); + + int removeByClusterId(long clusterId); } diff --git a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/dao/KubernetesClusterAffinityGroupMapDaoImpl.java b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/dao/KubernetesClusterAffinityGroupMapDaoImpl.java index 9b802f33ccab..f84d4fb9eee3 100644 --- a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/dao/KubernetesClusterAffinityGroupMapDaoImpl.java +++ b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/dao/KubernetesClusterAffinityGroupMapDaoImpl.java @@ -28,12 +28,17 @@ public class KubernetesClusterAffinityGroupMapDaoImpl extends GenericDaoBase clusterIdAndNodeTypeSearch; + private final SearchBuilder clusterIdSearch; public KubernetesClusterAffinityGroupMapDaoImpl() { clusterIdAndNodeTypeSearch = createSearchBuilder(); clusterIdAndNodeTypeSearch.and("clusterId", clusterIdAndNodeTypeSearch.entity().getClusterId(), SearchCriteria.Op.EQ); clusterIdAndNodeTypeSearch.and("nodeType", clusterIdAndNodeTypeSearch.entity().getNodeType(), SearchCriteria.Op.EQ); clusterIdAndNodeTypeSearch.done(); + + clusterIdSearch = createSearchBuilder(); + clusterIdSearch.and("clusterId", clusterIdSearch.entity().getClusterId(), SearchCriteria.Op.EQ); + clusterIdSearch.done(); } @Override @@ -49,4 +54,11 @@ public List listAffinityGroupIdsByClusterIdAndNodeType(long clusterId, Str List maps = listByClusterIdAndNodeType(clusterId, nodeType); return maps.stream().map(KubernetesClusterAffinityGroupMapVO::getAffinityGroupId).collect(Collectors.toList()); } + + @Override + public int removeByClusterId(long clusterId) { + SearchCriteria sc = clusterIdSearch.create(); + sc.setParameters("clusterId", clusterId); + return remove(sc); + } } From e0d41831b704e11f57a1d9146a192177ddf4598d Mon Sep 17 00:00:00 2001 From: Daman Arora Date: Wed, 7 Jan 2026 09:44:54 -0500 Subject: [PATCH 18/26] use @component for spring bean --- .../cluster/dao/KubernetesClusterAffinityGroupMapDaoImpl.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/dao/KubernetesClusterAffinityGroupMapDaoImpl.java b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/dao/KubernetesClusterAffinityGroupMapDaoImpl.java index f84d4fb9eee3..8b51d1b48c9e 100644 --- a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/dao/KubernetesClusterAffinityGroupMapDaoImpl.java +++ b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/dao/KubernetesClusterAffinityGroupMapDaoImpl.java @@ -19,11 +19,14 @@ import java.util.List; import java.util.stream.Collectors; +import org.springframework.stereotype.Component; + import com.cloud.kubernetes.cluster.KubernetesClusterAffinityGroupMapVO; import com.cloud.utils.db.GenericDaoBase; import com.cloud.utils.db.SearchBuilder; import com.cloud.utils.db.SearchCriteria; +@Component public class KubernetesClusterAffinityGroupMapDaoImpl extends GenericDaoBase implements KubernetesClusterAffinityGroupMapDao { From 201e5639e95758efd6826e8ff0c45cde398b1409 Mon Sep 17 00:00:00 2001 From: Daman Arora Date: Wed, 7 Jan 2026 09:45:29 -0500 Subject: [PATCH 19/26] remove affinity group on cleanup in mcloud managed cks --- .../cloud/kubernetes/cluster/KubernetesClusterManagerImpl.java | 1 - .../cluster/actionworkers/KubernetesClusterDestroyWorker.java | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) 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 75e764c52fa7..92518c43a61d 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 @@ -2068,7 +2068,6 @@ public boolean deleteKubernetesCluster(DeleteKubernetesClusterCmd cmd) throws Cl return Transaction.execute((TransactionCallback) status -> { kubernetesClusterDetailsDao.removeDetails(kubernetesClusterId); kubernetesClusterVmMapDao.removeByClusterId(kubernetesClusterId); - kubernetesClusterAffinityGroupMapDao.removeByClusterId(kubernetesClusterId); if (kubernetesClusterDao.remove(kubernetesClusterId)) { deleteProjectKubernetesAccountIfNeeded(cluster); return true; diff --git a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterDestroyWorker.java b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterDestroyWorker.java index 62bd8b4576a4..dc886117b22e 100644 --- a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterDestroyWorker.java +++ b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterDestroyWorker.java @@ -348,6 +348,7 @@ public boolean destroy() throws CloudRuntimeException { stateTransitTo(kubernetesCluster.getId(), KubernetesCluster.Event.OperationSucceeded); annotationDao.removeByEntityType(AnnotationService.EntityType.KUBERNETES_CLUSTER.name(), kubernetesCluster.getUuid()); kubernetesClusterDetailsDao.removeDetails(kubernetesCluster.getId()); + kubernetesClusterAffinityGroupMapDao.removeByClusterId(kubernetesCluster.getId()); boolean deleted = kubernetesClusterDao.remove(kubernetesCluster.getId()); if (!deleted) { logMessage(Level.WARN, String.format("Failed to delete Kubernetes cluster: %s", kubernetesCluster), null); From 240524941457242da1ff87587ea7002c7a5a1bdf Mon Sep 17 00:00:00 2001 From: Daman Arora Date: Wed, 7 Jan 2026 12:29:18 -0500 Subject: [PATCH 20/26] add affinty groups to cks list response --- .../apache/cloudstack/api/ApiConstants.java | 6 +++ .../cluster/KubernetesClusterManagerImpl.java | 35 ++++++++++++++ .../response/KubernetesClusterResponse.java | 48 +++++++++++++++++++ 3 files changed, 89 insertions(+) diff --git a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java index cd75bc025f2e..650f21284b93 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -1237,6 +1237,12 @@ public class ApiConstants { public static final String NODE_TYPE_OFFERING_MAP = "nodeofferings"; public static final String NODE_TYPE_TEMPLATE_MAP = "nodetemplates"; public static final String NODE_TYPE_AFFINITY_GROUP_MAP = "nodeaffinitygroups"; + public static final String CONTROL_AFFINITY_GROUP_IDS = "controlaffinitygroupids"; + public static final String CONTROL_AFFINITY_GROUP_NAMES = "controlaffinitygroupnames"; + public static final String WORKER_AFFINITY_GROUP_IDS = "workeraffinitygroupids"; + public static final String WORKER_AFFINITY_GROUP_NAMES = "workeraffinitygroupnames"; + public static final String ETCD_AFFINITY_GROUP_IDS = "etcdaffinitygroupids"; + public static final String ETCD_AFFINITY_GROUP_NAMES = "etcdaffinitygroupnames"; public static final String BOOT_TYPE = "boottype"; public static final String BOOT_MODE = "bootmode"; 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 92518c43a61d..05c3a7751d8e 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 @@ -908,10 +908,45 @@ public KubernetesClusterResponse createKubernetesClusterResponse(long kubernetes response.setClusterType(kubernetesCluster.getClusterType()); response.setCsiEnabled(kubernetesCluster.isCsiEnabled()); response.setCreated(kubernetesCluster.getCreated()); + setNodeTypeAffinityGroupResponse(response, kubernetesCluster.getId()); return response; } + protected void setNodeTypeAffinityGroupResponse(KubernetesClusterResponse response, long clusterId) { + setAffinityGroupResponseForNodeType(response, clusterId, CONTROL.name()); + setAffinityGroupResponseForNodeType(response, clusterId, WORKER.name()); + setAffinityGroupResponseForNodeType(response, clusterId, ETCD.name()); + } + + protected void setAffinityGroupResponseForNodeType(KubernetesClusterResponse response, long clusterId, String nodeType) { + List affinityGroupIds = kubernetesClusterAffinityGroupMapDao.listAffinityGroupIdsByClusterIdAndNodeType(clusterId, nodeType); + if (affinityGroupIds == null || affinityGroupIds.isEmpty()) { + return; + } + List affinityGroupUuids = new ArrayList<>(); + List affinityGroupNames = new ArrayList<>(); + for (Long affinityGroupId : affinityGroupIds) { + AffinityGroupVO affinityGroup = affinityGroupDao.findById(affinityGroupId); + if (affinityGroup != null) { + affinityGroupUuids.add(affinityGroup.getUuid()); + affinityGroupNames.add(affinityGroup.getName()); + } + } + String affinityGroupUuidsCsv = String.join(",", affinityGroupUuids); + String affinityGroupNamesCsv = String.join(",", affinityGroupNames); + if (CONTROL.name().equals(nodeType)) { + response.setControlAffinityGroupIds(affinityGroupUuidsCsv); + response.setControlAffinityGroupNames(affinityGroupNamesCsv); + } else if (WORKER.name().equals(nodeType)) { + response.setWorkerAffinityGroupIds(affinityGroupUuidsCsv); + response.setWorkerAffinityGroupNames(affinityGroupNamesCsv); + } else if (ETCD.name().equals(nodeType)) { + response.setEtcdAffinityGroupIds(affinityGroupUuidsCsv); + response.setEtcdAffinityGroupNames(affinityGroupNamesCsv); + } + } + private DataCenter validateAndGetZoneForKubernetesCreateParameters(Long zoneId, Long networkId) { DataCenter zone = dataCenterDao.findById(zoneId); if (zone == null) { diff --git a/plugins/integrations/kubernetes-service/src/main/java/org/apache/cloudstack/api/response/KubernetesClusterResponse.java b/plugins/integrations/kubernetes-service/src/main/java/org/apache/cloudstack/api/response/KubernetesClusterResponse.java index 0a7e7a97939d..932d722de354 100644 --- a/plugins/integrations/kubernetes-service/src/main/java/org/apache/cloudstack/api/response/KubernetesClusterResponse.java +++ b/plugins/integrations/kubernetes-service/src/main/java/org/apache/cloudstack/api/response/KubernetesClusterResponse.java @@ -220,6 +220,30 @@ public class KubernetesClusterResponse extends BaseResponseWithAnnotations imple @Param(description = "The date when this Kubernetes cluster was created") private Date created; + @SerializedName(ApiConstants.CONTROL_AFFINITY_GROUP_IDS) + @Param(description = "The IDs of affinity groups associated with control nodes", since = "4.23.0") + private String controlAffinityGroupIds; + + @SerializedName(ApiConstants.CONTROL_AFFINITY_GROUP_NAMES) + @Param(description = "The names of affinity groups associated with control nodes", since = "4.23.0") + private String controlAffinityGroupNames; + + @SerializedName(ApiConstants.WORKER_AFFINITY_GROUP_IDS) + @Param(description = "The IDs of affinity groups associated with worker nodes", since = "4.23.0") + private String workerAffinityGroupIds; + + @SerializedName(ApiConstants.WORKER_AFFINITY_GROUP_NAMES) + @Param(description = "The names of affinity groups associated with worker nodes", since = "4.23.0") + private String workerAffinityGroupNames; + + @SerializedName(ApiConstants.ETCD_AFFINITY_GROUP_IDS) + @Param(description = "The IDs of affinity groups associated with etcd nodes", since = "4.23.0") + private String etcdAffinityGroupIds; + + @SerializedName(ApiConstants.ETCD_AFFINITY_GROUP_NAMES) + @Param(description = "The names of affinity groups associated with etcd nodes", since = "4.23.0") + private String etcdAffinityGroupNames; + public KubernetesClusterResponse() { } @@ -535,4 +559,28 @@ public void setCniConfigName(String cniConfigName) { public void setCsiEnabled(Boolean csiEnabled) { isCsiEnabled = csiEnabled; } + + public void setControlAffinityGroupIds(String controlAffinityGroupIds) { + this.controlAffinityGroupIds = controlAffinityGroupIds; + } + + public void setControlAffinityGroupNames(String controlAffinityGroupNames) { + this.controlAffinityGroupNames = controlAffinityGroupNames; + } + + public void setWorkerAffinityGroupIds(String workerAffinityGroupIds) { + this.workerAffinityGroupIds = workerAffinityGroupIds; + } + + public void setWorkerAffinityGroupNames(String workerAffinityGroupNames) { + this.workerAffinityGroupNames = workerAffinityGroupNames; + } + + public void setEtcdAffinityGroupIds(String etcdAffinityGroupIds) { + this.etcdAffinityGroupIds = etcdAffinityGroupIds; + } + + public void setEtcdAffinityGroupNames(String etcdAffinityGroupNames) { + this.etcdAffinityGroupNames = etcdAffinityGroupNames; + } } From 96c0705b105900ac00114c244c29bb1fd3bab14e Mon Sep 17 00:00:00 2001 From: Daman Arora Date: Wed, 7 Jan 2026 13:24:21 -0500 Subject: [PATCH 21/26] cleanup --- .../src/main/resources/META-INF/db/schema-42210to42300.sql | 1 - 1 file changed, 1 deletion(-) diff --git a/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql b/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql index 9048823e7a60..a25074c18964 100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql @@ -19,7 +19,6 @@ -- Schema upgrade from 4.22.1.0 to 4.23.0.0 --; - -- Update value to random for the config 'vm.allocation.algorithm' or 'volume.allocation.algorithm' if configured as userconcentratedpod_random -- Update value to firstfit for the config 'vm.allocation.algorithm' or 'volume.allocation.algorithm' if configured as userconcentratedpod_firstfit UPDATE `cloud`.`configuration` SET value='random' WHERE name IN ('vm.allocation.algorithm', 'volume.allocation.algorithm') AND value='userconcentratedpod_random'; From a05581cec3075aa48bb534342ff85e2ac52f3a58 Mon Sep 17 00:00:00 2001 From: Daman Arora Date: Thu, 8 Jan 2026 11:27:29 -0500 Subject: [PATCH 22/26] add unit tests --- ...bernetesClusterAffinityGroupMapVOTest.java | 87 ++++++++++++ .../KubernetesClusterManagerImplTest.java | 134 ++++++++++++++++++ .../KubernetesClusterActionWorkerTest.java | 99 +++++++++++++ 3 files changed, 320 insertions(+) create mode 100644 plugins/integrations/kubernetes-service/src/test/java/com/cloud/kubernetes/cluster/KubernetesClusterAffinityGroupMapVOTest.java diff --git a/plugins/integrations/kubernetes-service/src/test/java/com/cloud/kubernetes/cluster/KubernetesClusterAffinityGroupMapVOTest.java b/plugins/integrations/kubernetes-service/src/test/java/com/cloud/kubernetes/cluster/KubernetesClusterAffinityGroupMapVOTest.java new file mode 100644 index 000000000000..d0aafc7d1e5e --- /dev/null +++ b/plugins/integrations/kubernetes-service/src/test/java/com/cloud/kubernetes/cluster/KubernetesClusterAffinityGroupMapVOTest.java @@ -0,0 +1,87 @@ +// 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 org.junit.Assert; +import org.junit.Test; + +public class KubernetesClusterAffinityGroupMapVOTest { + + @Test + public void testConstructorAndGetters() { + KubernetesClusterAffinityGroupMapVO vo = + new KubernetesClusterAffinityGroupMapVO(1L, "CONTROL", 100L); + + Assert.assertEquals(1L, vo.getClusterId()); + Assert.assertEquals("CONTROL", vo.getNodeType()); + Assert.assertEquals(100L, vo.getAffinityGroupId()); + } + + @Test + public void testDefaultConstructor() { + KubernetesClusterAffinityGroupMapVO vo = new KubernetesClusterAffinityGroupMapVO(); + Assert.assertNotNull(vo); + } + + @Test + public void testSetClusterId() { + KubernetesClusterAffinityGroupMapVO vo = new KubernetesClusterAffinityGroupMapVO(); + vo.setClusterId(2L); + Assert.assertEquals(2L, vo.getClusterId()); + } + + @Test + public void testSetNodeType() { + KubernetesClusterAffinityGroupMapVO vo = new KubernetesClusterAffinityGroupMapVO(); + vo.setNodeType("WORKER"); + Assert.assertEquals("WORKER", vo.getNodeType()); + } + + @Test + public void testSetAffinityGroupId() { + KubernetesClusterAffinityGroupMapVO vo = new KubernetesClusterAffinityGroupMapVO(); + vo.setAffinityGroupId(200L); + Assert.assertEquals(200L, vo.getAffinityGroupId()); + } + + @Test + public void testAllNodeTypes() { + KubernetesClusterAffinityGroupMapVO controlVo = + new KubernetesClusterAffinityGroupMapVO(1L, "CONTROL", 10L); + KubernetesClusterAffinityGroupMapVO workerVo = + new KubernetesClusterAffinityGroupMapVO(1L, "WORKER", 20L); + KubernetesClusterAffinityGroupMapVO etcdVo = + new KubernetesClusterAffinityGroupMapVO(1L, "ETCD", 30L); + + Assert.assertEquals("CONTROL", controlVo.getNodeType()); + Assert.assertEquals("WORKER", workerVo.getNodeType()); + Assert.assertEquals("ETCD", etcdVo.getNodeType()); + } + + @Test + public void testSettersChain() { + KubernetesClusterAffinityGroupMapVO vo = new KubernetesClusterAffinityGroupMapVO(); + + vo.setClusterId(5L); + vo.setNodeType("ETCD"); + vo.setAffinityGroupId(500L); + + Assert.assertEquals(5L, vo.getClusterId()); + Assert.assertEquals("ETCD", vo.getNodeType()); + Assert.assertEquals(500L, vo.getAffinityGroupId()); + } +} 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 index 2a381f282de2..9c5ca5fa110a 100644 --- 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 @@ -26,6 +26,7 @@ import com.cloud.exception.InvalidParameterValueException; import com.cloud.exception.PermissionDeniedException; import com.cloud.kubernetes.cluster.actionworkers.KubernetesClusterActionWorker; +import com.cloud.kubernetes.cluster.dao.KubernetesClusterAffinityGroupMapDao; import com.cloud.kubernetes.cluster.dao.KubernetesClusterDao; import com.cloud.kubernetes.cluster.dao.KubernetesClusterVmMapDao; import com.cloud.kubernetes.version.KubernetesSupportedVersion; @@ -46,9 +47,12 @@ import com.cloud.utils.net.NetUtils; import com.cloud.vm.VMInstanceVO; import com.cloud.vm.dao.VMInstanceDao; +import org.apache.cloudstack.affinity.AffinityGroupVO; +import org.apache.cloudstack.affinity.dao.AffinityGroupDao; import org.apache.cloudstack.api.BaseCmd; import org.apache.cloudstack.api.command.user.kubernetes.cluster.AddVirtualMachinesToKubernetesClusterCmd; import org.apache.cloudstack.api.command.user.kubernetes.cluster.RemoveVirtualMachinesFromKubernetesClusterCmd; +import org.apache.cloudstack.api.response.KubernetesClusterResponse; import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.framework.config.ConfigKey; import org.apache.commons.collections.MapUtils; @@ -103,6 +107,12 @@ public class KubernetesClusterManagerImplTest { @Mock private ServiceOfferingDao serviceOfferingDao; + @Mock + private KubernetesClusterAffinityGroupMapDao kubernetesClusterAffinityGroupMapDao; + + @Mock + private AffinityGroupDao affinityGroupDao; + @Spy @InjectMocks KubernetesClusterManagerImpl kubernetesClusterManager; @@ -441,4 +451,128 @@ public void testGetCksClusterPreferredArchSameArch() { String cksClusterPreferredArch = kubernetesClusterManager.getCksClusterPreferredArch(systemVMArch, cksIso); Assert.assertEquals(CPU.CPUArch.amd64.getType(), cksClusterPreferredArch); } + + @Test + public void testSetAffinityGroupResponseForNodeTypeControl() { + KubernetesClusterResponse response = new KubernetesClusterResponse(); + long clusterId = 1L; + + AffinityGroupVO ag1 = Mockito.mock(AffinityGroupVO.class); + AffinityGroupVO ag2 = Mockito.mock(AffinityGroupVO.class); + Mockito.when(ag1.getUuid()).thenReturn("uuid-1"); + Mockito.when(ag1.getName()).thenReturn("affinity-group-1"); + Mockito.when(ag2.getUuid()).thenReturn("uuid-2"); + Mockito.when(ag2.getName()).thenReturn("affinity-group-2"); + + Mockito.when(kubernetesClusterAffinityGroupMapDao.listAffinityGroupIdsByClusterIdAndNodeType(clusterId, CONTROL.name())) + .thenReturn(Arrays.asList(1L, 2L)); + Mockito.when(affinityGroupDao.findById(1L)).thenReturn(ag1); + Mockito.when(affinityGroupDao.findById(2L)).thenReturn(ag2); + + kubernetesClusterManager.setAffinityGroupResponseForNodeType(response, clusterId, CONTROL.name()); + + Mockito.verify(kubernetesClusterAffinityGroupMapDao).listAffinityGroupIdsByClusterIdAndNodeType(clusterId, CONTROL.name()); + Mockito.verify(affinityGroupDao).findById(1L); + Mockito.verify(affinityGroupDao).findById(2L); + } + + @Test + public void testSetAffinityGroupResponseForNodeTypeWorker() { + KubernetesClusterResponse response = new KubernetesClusterResponse(); + long clusterId = 1L; + + AffinityGroupVO ag = Mockito.mock(AffinityGroupVO.class); + Mockito.when(ag.getUuid()).thenReturn("worker-uuid"); + Mockito.when(ag.getName()).thenReturn("worker-affinity"); + + Mockito.when(kubernetesClusterAffinityGroupMapDao.listAffinityGroupIdsByClusterIdAndNodeType(clusterId, WORKER.name())) + .thenReturn(Arrays.asList(10L)); + Mockito.when(affinityGroupDao.findById(10L)).thenReturn(ag); + + kubernetesClusterManager.setAffinityGroupResponseForNodeType(response, clusterId, WORKER.name()); + + Mockito.verify(kubernetesClusterAffinityGroupMapDao).listAffinityGroupIdsByClusterIdAndNodeType(clusterId, WORKER.name()); + Mockito.verify(affinityGroupDao).findById(10L); + } + + @Test + public void testSetAffinityGroupResponseForNodeTypeEtcd() { + KubernetesClusterResponse response = new KubernetesClusterResponse(); + long clusterId = 1L; + + AffinityGroupVO ag = Mockito.mock(AffinityGroupVO.class); + Mockito.when(ag.getUuid()).thenReturn("etcd-uuid"); + Mockito.when(ag.getName()).thenReturn("etcd-affinity"); + + Mockito.when(kubernetesClusterAffinityGroupMapDao.listAffinityGroupIdsByClusterIdAndNodeType(clusterId, ETCD.name())) + .thenReturn(Arrays.asList(20L)); + Mockito.when(affinityGroupDao.findById(20L)).thenReturn(ag); + + kubernetesClusterManager.setAffinityGroupResponseForNodeType(response, clusterId, ETCD.name()); + + Mockito.verify(kubernetesClusterAffinityGroupMapDao).listAffinityGroupIdsByClusterIdAndNodeType(clusterId, ETCD.name()); + Mockito.verify(affinityGroupDao).findById(20L); + } + + @Test + public void testSetAffinityGroupResponseForNodeTypeEmptyList() { + KubernetesClusterResponse response = new KubernetesClusterResponse(); + long clusterId = 1L; + + Mockito.when(kubernetesClusterAffinityGroupMapDao.listAffinityGroupIdsByClusterIdAndNodeType(clusterId, CONTROL.name())) + .thenReturn(Collections.emptyList()); + + kubernetesClusterManager.setAffinityGroupResponseForNodeType(response, clusterId, CONTROL.name()); + + Mockito.verify(affinityGroupDao, Mockito.never()).findById(Mockito.anyLong()); + } + + @Test + public void testSetAffinityGroupResponseForNodeTypeNullList() { + KubernetesClusterResponse response = new KubernetesClusterResponse(); + long clusterId = 1L; + + Mockito.when(kubernetesClusterAffinityGroupMapDao.listAffinityGroupIdsByClusterIdAndNodeType(clusterId, ETCD.name())) + .thenReturn(null); + + kubernetesClusterManager.setAffinityGroupResponseForNodeType(response, clusterId, ETCD.name()); + + Mockito.verify(affinityGroupDao, Mockito.never()).findById(Mockito.anyLong()); + } + + @Test + public void testSetAffinityGroupResponseForNodeTypeNullAffinityGroup() { + KubernetesClusterResponse response = new KubernetesClusterResponse(); + long clusterId = 1L; + + AffinityGroupVO ag1 = Mockito.mock(AffinityGroupVO.class); + Mockito.when(ag1.getUuid()).thenReturn("uuid-1"); + Mockito.when(ag1.getName()).thenReturn("affinity-group-1"); + + Mockito.when(kubernetesClusterAffinityGroupMapDao.listAffinityGroupIdsByClusterIdAndNodeType(clusterId, CONTROL.name())) + .thenReturn(Arrays.asList(1L, 2L)); + Mockito.when(affinityGroupDao.findById(1L)).thenReturn(ag1); + Mockito.when(affinityGroupDao.findById(2L)).thenReturn(null); + + kubernetesClusterManager.setAffinityGroupResponseForNodeType(response, clusterId, CONTROL.name()); + + Mockito.verify(affinityGroupDao).findById(1L); + Mockito.verify(affinityGroupDao).findById(2L); + } + + @Test + public void testSetNodeTypeAffinityGroupResponse() { + KubernetesClusterResponse response = new KubernetesClusterResponse(); + long clusterId = 1L; + + Mockito.when(kubernetesClusterAffinityGroupMapDao.listAffinityGroupIdsByClusterIdAndNodeType(Mockito.eq(clusterId), Mockito.anyString())) + .thenReturn(Collections.emptyList()); + + kubernetesClusterManager.setNodeTypeAffinityGroupResponse(response, clusterId); + + Mockito.verify(kubernetesClusterAffinityGroupMapDao).listAffinityGroupIdsByClusterIdAndNodeType(clusterId, CONTROL.name()); + Mockito.verify(kubernetesClusterAffinityGroupMapDao).listAffinityGroupIdsByClusterIdAndNodeType(clusterId, WORKER.name()); + Mockito.verify(kubernetesClusterAffinityGroupMapDao).listAffinityGroupIdsByClusterIdAndNodeType(clusterId, ETCD.name()); + } + } diff --git a/plugins/integrations/kubernetes-service/src/test/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterActionWorkerTest.java b/plugins/integrations/kubernetes-service/src/test/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterActionWorkerTest.java index 1eb55808e09d..a25ec55cc04a 100644 --- a/plugins/integrations/kubernetes-service/src/test/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterActionWorkerTest.java +++ b/plugins/integrations/kubernetes-service/src/test/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterActionWorkerTest.java @@ -16,8 +16,14 @@ // under the License. package com.cloud.kubernetes.cluster.actionworkers; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; import java.util.UUID; +import org.apache.cloudstack.affinity.AffinityGroupVO; +import org.apache.cloudstack.affinity.dao.AffinityGroupDao; import org.apache.cloudstack.api.ApiConstants; import org.junit.Assert; import org.junit.Before; @@ -30,6 +36,8 @@ import com.cloud.kubernetes.cluster.KubernetesCluster; import com.cloud.kubernetes.cluster.KubernetesClusterDetailsVO; import com.cloud.kubernetes.cluster.KubernetesClusterManagerImpl; +import com.cloud.kubernetes.cluster.KubernetesServiceHelper.KubernetesClusterNodeType; +import com.cloud.kubernetes.cluster.dao.KubernetesClusterAffinityGroupMapDao; import com.cloud.kubernetes.cluster.dao.KubernetesClusterDao; import com.cloud.kubernetes.cluster.dao.KubernetesClusterDetailsDao; import com.cloud.kubernetes.cluster.dao.KubernetesClusterVmMapDao; @@ -60,6 +68,12 @@ public class KubernetesClusterActionWorkerTest { @Mock IPAddressDao ipAddressDao; + @Mock + KubernetesClusterAffinityGroupMapDao kubernetesClusterAffinityGroupMapDao; + + @Mock + AffinityGroupDao affinityGroupDao; + KubernetesClusterActionWorker actionWorker = null; final static Long DEFAULT_ID = 1L; @@ -70,10 +84,12 @@ public void setUp() throws Exception { kubernetesClusterManager.kubernetesSupportedVersionDao = kubernetesSupportedVersionDao; kubernetesClusterManager.kubernetesClusterDetailsDao = kubernetesClusterDetailsDao; kubernetesClusterManager.kubernetesClusterVmMapDao = kubernetesClusterVmMapDao; + kubernetesClusterManager.kubernetesClusterAffinityGroupMapDao = kubernetesClusterAffinityGroupMapDao; KubernetesCluster kubernetesCluster = Mockito.mock(KubernetesCluster.class); Mockito.when(kubernetesCluster.getId()).thenReturn(DEFAULT_ID); actionWorker = new KubernetesClusterActionWorker(kubernetesCluster, kubernetesClusterManager); actionWorker.ipAddressDao = ipAddressDao; + actionWorker.affinityGroupDao = affinityGroupDao; } @Test @@ -130,4 +146,87 @@ public void testGetVpcTierKubernetesPublicIpValid() { IpAddress result = actionWorker.getVpcTierKubernetesPublicIp(mockNetworkForGetVpcTierKubernetesPublicIpTest()); Assert.assertNotNull(result); } + + @Test + public void testGetAffinityGroupIdsForNodeTypeReturnsIds() { + Mockito.when(kubernetesClusterAffinityGroupMapDao.listAffinityGroupIdsByClusterIdAndNodeType(DEFAULT_ID, "CONTROL")) + .thenReturn(Arrays.asList(1L, 2L)); + + List result = actionWorker.getAffinityGroupIdsForNodeType(KubernetesClusterNodeType.CONTROL); + + Assert.assertEquals(2, result.size()); + Assert.assertTrue(result.containsAll(Arrays.asList(1L, 2L))); + } + + @Test + public void testGetAffinityGroupIdsForNodeTypeReturnsEmptyList() { + Mockito.when(kubernetesClusterAffinityGroupMapDao.listAffinityGroupIdsByClusterIdAndNodeType(DEFAULT_ID, "WORKER")) + .thenReturn(Collections.emptyList()); + + List result = actionWorker.getAffinityGroupIdsForNodeType(KubernetesClusterNodeType.WORKER); + + Assert.assertTrue(result.isEmpty()); + } + + @Test + public void testGetMergedAffinityGroupIdsWithExplicitDedication() { + Mockito.when(kubernetesClusterAffinityGroupMapDao.listAffinityGroupIdsByClusterIdAndNodeType(DEFAULT_ID, "CONTROL")) + .thenReturn(new ArrayList<>(Arrays.asList(1L))); + + AffinityGroupVO explicitGroup = Mockito.mock(AffinityGroupVO.class); + Mockito.when(explicitGroup.getId()).thenReturn(99L); + Mockito.when(affinityGroupDao.findByAccountAndType(Mockito.anyLong(), Mockito.eq("ExplicitDedication"))) + .thenReturn(explicitGroup); + + List result = actionWorker.getMergedAffinityGroupIds(KubernetesClusterNodeType.CONTROL, 1L, 1L); + + Assert.assertEquals(2, result.size()); + Assert.assertTrue(result.contains(1L)); + Assert.assertTrue(result.contains(99L)); + } + + @Test + public void testGetMergedAffinityGroupIdsNoExplicitDedication() { + Mockito.when(kubernetesClusterAffinityGroupMapDao.listAffinityGroupIdsByClusterIdAndNodeType(DEFAULT_ID, "WORKER")) + .thenReturn(new ArrayList<>(Arrays.asList(1L, 2L))); + Mockito.when(affinityGroupDao.findByAccountAndType(Mockito.anyLong(), Mockito.eq("ExplicitDedication"))) + .thenReturn(null); + Mockito.when(affinityGroupDao.findDomainLevelGroupByType(Mockito.anyLong(), Mockito.eq("ExplicitDedication"))) + .thenReturn(null); + + List result = actionWorker.getMergedAffinityGroupIds(KubernetesClusterNodeType.WORKER, 1L, 1L); + + Assert.assertEquals(2, result.size()); + } + + @Test + public void testGetMergedAffinityGroupIdsReturnsNullWhenEmpty() { + Mockito.when(kubernetesClusterAffinityGroupMapDao.listAffinityGroupIdsByClusterIdAndNodeType(DEFAULT_ID, "ETCD")) + .thenReturn(new ArrayList<>()); + Mockito.when(affinityGroupDao.findByAccountAndType(Mockito.anyLong(), Mockito.anyString())) + .thenReturn(null); + Mockito.when(affinityGroupDao.findDomainLevelGroupByType(Mockito.anyLong(), Mockito.anyString())) + .thenReturn(null); + + List result = actionWorker.getMergedAffinityGroupIds(KubernetesClusterNodeType.ETCD, 1L, 1L); + + Assert.assertNull(result); + } + + @Test + public void testGetMergedAffinityGroupIdsExplicitDedicationAlreadyInList() { + Mockito.when(kubernetesClusterAffinityGroupMapDao.listAffinityGroupIdsByClusterIdAndNodeType(DEFAULT_ID, "CONTROL")) + .thenReturn(new ArrayList<>(Arrays.asList(99L, 2L))); + + AffinityGroupVO explicitGroup = Mockito.mock(AffinityGroupVO.class); + Mockito.when(explicitGroup.getId()).thenReturn(99L); + Mockito.when(affinityGroupDao.findByAccountAndType(Mockito.anyLong(), Mockito.eq("ExplicitDedication"))) + .thenReturn(explicitGroup); + + List result = actionWorker.getMergedAffinityGroupIds(KubernetesClusterNodeType.CONTROL, 1L, 1L); + + Assert.assertEquals(2, result.size()); + Assert.assertTrue(result.contains(99L)); + Assert.assertTrue(result.contains(2L)); + } } From 8f5ee6dae37ca1887eacb36d94ef9c598858943c Mon Sep 17 00:00:00 2001 From: Daman Arora Date: Thu, 8 Jan 2026 13:27:48 -0500 Subject: [PATCH 23/26] add affinity group details to user VM response --- .../cloud/kubernetes/cluster/KubernetesClusterManagerImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 05c3a7751d8e..524f585b836c 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 @@ -872,7 +872,7 @@ public KubernetesClusterResponse createKubernetesClusterResponse(long kubernetes UserVmJoinVO userVM = userVmJoinDao.findById(vmMapVO.getVmId()); if (userVM != null) { UserVmResponse vmResponse = ApiDBUtils.newUserVmResponse(respView, responseName, userVM, - EnumSet.of(VMDetails.nics), caller); + EnumSet.of(VMDetails.nics, VMDetails.affgrp), caller); KubernetesUserVmResponse kubernetesUserVmResponse = new KubernetesUserVmResponse(); try { BeanUtils.copyProperties(kubernetesUserVmResponse, vmResponse); From d27b2f45be5e460db1081bd94dcf0396988bbdbb Mon Sep 17 00:00:00 2001 From: Daman Arora Date: Thu, 8 Jan 2026 14:30:13 -0500 Subject: [PATCH 24/26] update user VM response handling in KubernetesClusterManagerImpl --- .../cluster/KubernetesClusterManagerImpl.java | 30 ++++++++++++++----- 1 file changed, 22 insertions(+), 8 deletions(-) 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 524f585b836c..2dad9cc26a71 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 @@ -861,24 +861,38 @@ public KubernetesClusterResponse createKubernetesClusterResponse(long kubernetes List vmResponses = new ArrayList<>(); List vmList = kubernetesClusterVmMapDao.listByClusterId(kubernetesCluster.getId()); - ResponseView respView = ResponseView.Restricted; + ResponseView userVmResponseView = ResponseView.Restricted; Account caller = CallContext.current().getCallingAccount(); if (accountService.isRootAdmin(caller.getId())) { - respView = ResponseView.Full; + userVmResponseView = ResponseView.Full; } final String responseName = "virtualmachine"; if (vmList != null && !vmList.isEmpty()) { - for (KubernetesClusterVmMapVO vmMapVO : vmList) { - UserVmJoinVO userVM = userVmJoinDao.findById(vmMapVO.getVmId()); - if (userVM != null) { - UserVmResponse vmResponse = ApiDBUtils.newUserVmResponse(respView, responseName, userVM, - EnumSet.of(VMDetails.nics, VMDetails.affgrp), caller); + Map vmMapById = vmList.stream() + .collect(Collectors.toMap(KubernetesClusterVmMapVO::getVmId, vm -> vm)); + Long[] vmIds = vmMapById.keySet().toArray(new Long[0]); + List userVmJoinVOs = userVmJoinDao.searchByIds(vmIds); + if (userVmJoinVOs != null && !userVmJoinVOs.isEmpty()) { + Map vmResponseMap = new HashMap<>(); + for (UserVmJoinVO userVM : userVmJoinVOs) { + Long vmId = userVM.getId(); + UserVmResponse vmResponse = vmResponseMap.get(vmId); + if (vmResponse == null) { + vmResponse = ApiDBUtils.newUserVmResponse(userVmResponseView, responseName, userVM, + EnumSet.of(VMDetails.nics, VMDetails.affgrp), caller); + vmResponseMap.put(vmId, vmResponse); + } else { + ApiDBUtils.fillVmDetails(userVmResponseView, vmResponse, userVM); + } + } + for (Map.Entry vmIdResponseEntry : vmResponseMap.entrySet()) { KubernetesUserVmResponse kubernetesUserVmResponse = new KubernetesUserVmResponse(); try { - BeanUtils.copyProperties(kubernetesUserVmResponse, vmResponse); + BeanUtils.copyProperties(kubernetesUserVmResponse, vmIdResponseEntry.getValue()); } catch (IllegalAccessException | InvocationTargetException e) { throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to generate zone metrics response"); } + KubernetesClusterVmMapVO vmMapVO = vmMapById.get(vmIdResponseEntry.getKey()); kubernetesUserVmResponse.setExternalNode(vmMapVO.isExternalNode()); kubernetesUserVmResponse.setEtcdNode(vmMapVO.isEtcdNode()); kubernetesUserVmResponse.setNodeVersion(vmMapVO.getNodeVersion()); From cd37b81147e3628ca4c853b87f27bb30184cfe85 Mon Sep 17 00:00:00 2001 From: Daman Arora Date: Mon, 12 Jan 2026 09:16:03 -0500 Subject: [PATCH 25/26] implement node affinity group validation method --- .../cluster/KubernetesClusterManagerImpl.java | 78 ++++ .../KubernetesClusterManagerImplTest.java | 339 ++++++++++++++++++ 2 files changed, 417 insertions(+) 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 2dad9cc26a71..71bd460916aa 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 @@ -2357,6 +2357,7 @@ public boolean addNodesToKubernetesCluster(AddNodesToKubernetesClusterCmd cmd) { if (validNodeIds.isEmpty()) { throw new CloudRuntimeException("No valid nodes found to be added to the Kubernetes cluster"); } + validateNodeAffinityGroups(validNodeIds, kubernetesCluster); KubernetesClusterAddWorker addWorker = new KubernetesClusterAddWorker(kubernetesCluster, KubernetesClusterManagerImpl.this); addWorker = ComponentContext.inject(addWorker); return addWorker.addNodesToCluster(validNodeIds, cmd.isMountCksIsoOnVr(), cmd.isManualUpgrade()); @@ -2416,6 +2417,83 @@ private List validateNodes(List nodeIds, Long networkId, String netw return validNodeIds; } + protected void validateNodeAffinityGroups(List nodeIds, KubernetesCluster cluster) { + List workerAffinityGroupIds = kubernetesClusterAffinityGroupMapDao.listAffinityGroupIdsByClusterIdAndNodeType( + cluster.getId(), WORKER.name()); + if (CollectionUtils.isEmpty(workerAffinityGroupIds)) { + return; + } + + List existingWorkerVms = kubernetesClusterVmMapDao.listByClusterIdAndVmType(cluster.getId(), WORKER); + Set existingWorkerHostIds = new HashSet<>(); + for (KubernetesClusterVmMapVO workerVmMap : existingWorkerVms) { + VMInstanceVO workerVm = vmInstanceDao.findById(workerVmMap.getVmId()); + if (workerVm != null && workerVm.getHostId() != null) { + existingWorkerHostIds.add(workerVm.getHostId()); + } + } + + for (Long affinityGroupId : workerAffinityGroupIds) { + AffinityGroupVO affinityGroup = affinityGroupDao.findById(affinityGroupId); + if (affinityGroup == null) { + continue; + } + String affinityGroupType = affinityGroup.getType(); + + for (Long nodeId : nodeIds) { + VMInstanceVO node = vmInstanceDao.findById(nodeId); + if (node == null || node.getHostId() == null) { + continue; + } + Long nodeHostId = node.getHostId(); + HostVO nodeHost = hostDao.findById(nodeHostId); + String nodeHostName = nodeHost != null ? nodeHost.getName() : String.valueOf(nodeHostId); + + if ("host anti-affinity".equalsIgnoreCase(affinityGroupType)) { + if (existingWorkerHostIds.contains(nodeHostId)) { + throw new InvalidParameterValueException(String.format( + "Cannot add VM %s to cluster %s. VM is running on host %s which violates the cluster's " + + "host anti-affinity rule (affinity group: %s). Existing worker VMs are already running on this host.", + node.getInstanceName(), cluster.getName(), nodeHostName, affinityGroup.getName())); + } + } else if ("host affinity".equalsIgnoreCase(affinityGroupType)) { + if (!existingWorkerHostIds.isEmpty() && !existingWorkerHostIds.contains(nodeHostId)) { + List existingHostNames = new ArrayList<>(); + for (Long hostId : existingWorkerHostIds) { + HostVO host = hostDao.findById(hostId); + existingHostNames.add(host != null ? host.getName() : String.valueOf(hostId)); + } + throw new InvalidParameterValueException(String.format( + "Cannot add VM %s to cluster %s. VM is running on host %s which violates the cluster's " + + "host affinity rule (affinity group: %s). All worker VMs must run on the same host. " + + "Existing workers are on host(s): %s.", + node.getInstanceName(), cluster.getName(), nodeHostName, affinityGroup.getName(), + String.join(", ", existingHostNames))); + } + } + } + + if ("host anti-affinity".equalsIgnoreCase(affinityGroupType)) { + Set newNodeHostIds = new HashSet<>(); + for (Long nodeId : nodeIds) { + VMInstanceVO node = vmInstanceDao.findById(nodeId); + if (node != null && node.getHostId() != null) { + Long nodeHostId = node.getHostId(); + if (newNodeHostIds.contains(nodeHostId)) { + HostVO nodeHost = hostDao.findById(nodeHostId); + String nodeHostName = nodeHost != null ? nodeHost.getName() : String.valueOf(nodeHostId); + throw new InvalidParameterValueException(String.format( + "Cannot add VM %s to cluster %s. Multiple VMs being added are running on the same host %s, " + + "which violates the cluster's host anti-affinity rule (affinity group: %s).", + node.getInstanceName(), cluster.getName(), nodeHostName, affinityGroup.getName())); + } + newNodeHostIds.add(nodeHostId); + } + } + } + } + } + @Override public List removeVmsFromCluster(RemoveVirtualMachinesFromKubernetesClusterCmd cmd) { if (!KubernetesServiceEnabled.value()) { 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 index 9c5ca5fa110a..ee4d6429e12d 100644 --- 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 @@ -47,6 +47,8 @@ import com.cloud.utils.net.NetUtils; import com.cloud.vm.VMInstanceVO; import com.cloud.vm.dao.VMInstanceDao; +import com.cloud.host.HostVO; +import com.cloud.host.dao.HostDao; import org.apache.cloudstack.affinity.AffinityGroupVO; import org.apache.cloudstack.affinity.dao.AffinityGroupDao; import org.apache.cloudstack.api.BaseCmd; @@ -113,6 +115,9 @@ public class KubernetesClusterManagerImplTest { @Mock private AffinityGroupDao affinityGroupDao; + @Mock + private HostDao hostDao; + @Spy @InjectMocks KubernetesClusterManagerImpl kubernetesClusterManager; @@ -575,4 +580,338 @@ public void testSetNodeTypeAffinityGroupResponse() { Mockito.verify(kubernetesClusterAffinityGroupMapDao).listAffinityGroupIdsByClusterIdAndNodeType(clusterId, ETCD.name()); } + @Test + public void testValidateNodeAffinityGroupsNoAffinityGroups() { + KubernetesCluster cluster = Mockito.mock(KubernetesCluster.class); + Mockito.when(cluster.getId()).thenReturn(1L); + List nodeIds = Arrays.asList(100L, 101L); + + Mockito.when(kubernetesClusterAffinityGroupMapDao.listAffinityGroupIdsByClusterIdAndNodeType(1L, WORKER.name())) + .thenReturn(Collections.emptyList()); + + kubernetesClusterManager.validateNodeAffinityGroups(nodeIds, cluster); + + Mockito.verify(kubernetesClusterVmMapDao, Mockito.never()).listByClusterIdAndVmType(Mockito.anyLong(), Mockito.any()); + } + + @Test + public void testValidateNodeAffinityGroupsNullAffinityGroups() { + KubernetesCluster cluster = Mockito.mock(KubernetesCluster.class); + Mockito.when(cluster.getId()).thenReturn(1L); + List nodeIds = Arrays.asList(100L, 101L); + + Mockito.when(kubernetesClusterAffinityGroupMapDao.listAffinityGroupIdsByClusterIdAndNodeType(1L, WORKER.name())) + .thenReturn(null); + + kubernetesClusterManager.validateNodeAffinityGroups(nodeIds, cluster); + + Mockito.verify(kubernetesClusterVmMapDao, Mockito.never()).listByClusterIdAndVmType(Mockito.anyLong(), Mockito.any()); + } + + @Test(expected = InvalidParameterValueException.class) + public void testValidateNodeAffinityGroupsAntiAffinityNewNodeOnExistingHost() { + KubernetesCluster cluster = Mockito.mock(KubernetesCluster.class); + Mockito.when(cluster.getId()).thenReturn(1L); + Mockito.when(cluster.getName()).thenReturn("test-cluster"); + + Long newNodeId = 100L; + Long existingWorkerVmId = 200L; + Long sharedHostId = 1000L; + + AffinityGroupVO affinityGroup = Mockito.mock(AffinityGroupVO.class); + Mockito.when(affinityGroup.getType()).thenReturn("host anti-affinity"); + Mockito.when(affinityGroup.getName()).thenReturn("anti-affinity-group"); + + VMInstanceVO newNode = Mockito.mock(VMInstanceVO.class); + Mockito.when(newNode.getHostId()).thenReturn(sharedHostId); + Mockito.when(newNode.getInstanceName()).thenReturn("new-node-vm"); + + VMInstanceVO existingWorkerVm = Mockito.mock(VMInstanceVO.class); + Mockito.when(existingWorkerVm.getHostId()).thenReturn(sharedHostId); + + KubernetesClusterVmMapVO workerVmMap = Mockito.mock(KubernetesClusterVmMapVO.class); + Mockito.when(workerVmMap.getVmId()).thenReturn(existingWorkerVmId); + + HostVO host = Mockito.mock(HostVO.class); + Mockito.when(host.getName()).thenReturn("host-1"); + + Mockito.when(kubernetesClusterAffinityGroupMapDao.listAffinityGroupIdsByClusterIdAndNodeType(1L, WORKER.name())) + .thenReturn(Arrays.asList(10L)); + Mockito.when(affinityGroupDao.findById(10L)).thenReturn(affinityGroup); + Mockito.when(kubernetesClusterVmMapDao.listByClusterIdAndVmType(1L, WORKER)) + .thenReturn(Arrays.asList(workerVmMap)); + Mockito.when(vmInstanceDao.findById(existingWorkerVmId)).thenReturn(existingWorkerVm); + Mockito.when(vmInstanceDao.findById(newNodeId)).thenReturn(newNode); + Mockito.when(hostDao.findById(sharedHostId)).thenReturn(host); + + kubernetesClusterManager.validateNodeAffinityGroups(Arrays.asList(newNodeId), cluster); + } + + @Test + public void testValidateNodeAffinityGroupsAntiAffinityNewNodeOnDifferentHost() { + KubernetesCluster cluster = Mockito.mock(KubernetesCluster.class); + Mockito.when(cluster.getId()).thenReturn(1L); + Mockito.when(cluster.getName()).thenReturn("test-cluster"); + + Long newNodeId = 100L; + Long existingWorkerVmId = 200L; + Long existingHostId = 1000L; + Long newNodeHostId = 1001L; + + AffinityGroupVO affinityGroup = Mockito.mock(AffinityGroupVO.class); + Mockito.when(affinityGroup.getType()).thenReturn("host anti-affinity"); + Mockito.when(affinityGroup.getName()).thenReturn("anti-affinity-group"); + + VMInstanceVO newNode = Mockito.mock(VMInstanceVO.class); + Mockito.when(newNode.getHostId()).thenReturn(newNodeHostId); + + VMInstanceVO existingWorkerVm = Mockito.mock(VMInstanceVO.class); + Mockito.when(existingWorkerVm.getHostId()).thenReturn(existingHostId); + + KubernetesClusterVmMapVO workerVmMap = Mockito.mock(KubernetesClusterVmMapVO.class); + Mockito.when(workerVmMap.getVmId()).thenReturn(existingWorkerVmId); + + Mockito.when(kubernetesClusterAffinityGroupMapDao.listAffinityGroupIdsByClusterIdAndNodeType(1L, WORKER.name())) + .thenReturn(Arrays.asList(10L)); + Mockito.when(affinityGroupDao.findById(10L)).thenReturn(affinityGroup); + Mockito.when(kubernetesClusterVmMapDao.listByClusterIdAndVmType(1L, WORKER)) + .thenReturn(Arrays.asList(workerVmMap)); + Mockito.when(vmInstanceDao.findById(existingWorkerVmId)).thenReturn(existingWorkerVm); + Mockito.when(vmInstanceDao.findById(newNodeId)).thenReturn(newNode); + + kubernetesClusterManager.validateNodeAffinityGroups(Arrays.asList(newNodeId), cluster); + + Mockito.verify(kubernetesClusterAffinityGroupMapDao).listAffinityGroupIdsByClusterIdAndNodeType(1L, WORKER.name()); + } + + @Test + public void testValidateNodeAffinityGroupsAffinityNewNodeOnSameHost() { + KubernetesCluster cluster = Mockito.mock(KubernetesCluster.class); + Mockito.when(cluster.getId()).thenReturn(1L); + Mockito.when(cluster.getName()).thenReturn("test-cluster"); + + Long newNodeId = 100L; + Long existingWorkerVmId = 200L; + Long sharedHostId = 1000L; + + AffinityGroupVO affinityGroup = Mockito.mock(AffinityGroupVO.class); + Mockito.when(affinityGroup.getType()).thenReturn("host affinity"); + Mockito.when(affinityGroup.getName()).thenReturn("affinity-group"); + + VMInstanceVO newNode = Mockito.mock(VMInstanceVO.class); + Mockito.when(newNode.getHostId()).thenReturn(sharedHostId); + + VMInstanceVO existingWorkerVm = Mockito.mock(VMInstanceVO.class); + Mockito.when(existingWorkerVm.getHostId()).thenReturn(sharedHostId); + + KubernetesClusterVmMapVO workerVmMap = Mockito.mock(KubernetesClusterVmMapVO.class); + Mockito.when(workerVmMap.getVmId()).thenReturn(existingWorkerVmId); + + Mockito.when(kubernetesClusterAffinityGroupMapDao.listAffinityGroupIdsByClusterIdAndNodeType(1L, WORKER.name())) + .thenReturn(Arrays.asList(10L)); + Mockito.when(affinityGroupDao.findById(10L)).thenReturn(affinityGroup); + Mockito.when(kubernetesClusterVmMapDao.listByClusterIdAndVmType(1L, WORKER)) + .thenReturn(Arrays.asList(workerVmMap)); + Mockito.when(vmInstanceDao.findById(existingWorkerVmId)).thenReturn(existingWorkerVm); + Mockito.when(vmInstanceDao.findById(newNodeId)).thenReturn(newNode); + + kubernetesClusterManager.validateNodeAffinityGroups(Arrays.asList(newNodeId), cluster); + + Mockito.verify(kubernetesClusterAffinityGroupMapDao).listAffinityGroupIdsByClusterIdAndNodeType(1L, WORKER.name()); + } + + @Test(expected = InvalidParameterValueException.class) + public void testValidateNodeAffinityGroupsAffinityNewNodeOnDifferentHost() { + KubernetesCluster cluster = Mockito.mock(KubernetesCluster.class); + Mockito.when(cluster.getId()).thenReturn(1L); + Mockito.when(cluster.getName()).thenReturn("test-cluster"); + + Long newNodeId = 100L; + Long existingWorkerVmId = 200L; + Long existingHostId = 1000L; + Long newNodeHostId = 1001L; + + AffinityGroupVO affinityGroup = Mockito.mock(AffinityGroupVO.class); + Mockito.when(affinityGroup.getType()).thenReturn("host affinity"); + Mockito.when(affinityGroup.getName()).thenReturn("affinity-group"); + + VMInstanceVO newNode = Mockito.mock(VMInstanceVO.class); + Mockito.when(newNode.getHostId()).thenReturn(newNodeHostId); + Mockito.when(newNode.getInstanceName()).thenReturn("new-node-vm"); + + VMInstanceVO existingWorkerVm = Mockito.mock(VMInstanceVO.class); + Mockito.when(existingWorkerVm.getHostId()).thenReturn(existingHostId); + + KubernetesClusterVmMapVO workerVmMap = Mockito.mock(KubernetesClusterVmMapVO.class); + Mockito.when(workerVmMap.getVmId()).thenReturn(existingWorkerVmId); + + HostVO newHost = Mockito.mock(HostVO.class); + Mockito.when(newHost.getName()).thenReturn("host-2"); + + HostVO existingHost = Mockito.mock(HostVO.class); + Mockito.when(existingHost.getName()).thenReturn("host-1"); + + Mockito.when(kubernetesClusterAffinityGroupMapDao.listAffinityGroupIdsByClusterIdAndNodeType(1L, WORKER.name())) + .thenReturn(Arrays.asList(10L)); + Mockito.when(affinityGroupDao.findById(10L)).thenReturn(affinityGroup); + Mockito.when(kubernetesClusterVmMapDao.listByClusterIdAndVmType(1L, WORKER)) + .thenReturn(Arrays.asList(workerVmMap)); + Mockito.when(vmInstanceDao.findById(existingWorkerVmId)).thenReturn(existingWorkerVm); + Mockito.when(vmInstanceDao.findById(newNodeId)).thenReturn(newNode); + Mockito.when(hostDao.findById(newNodeHostId)).thenReturn(newHost); + Mockito.when(hostDao.findById(existingHostId)).thenReturn(existingHost); + + kubernetesClusterManager.validateNodeAffinityGroups(Arrays.asList(newNodeId), cluster); + } + + @Test(expected = InvalidParameterValueException.class) + public void testValidateNodeAffinityGroupsAntiAffinityMultipleNewNodesOnSameHost() { + KubernetesCluster cluster = Mockito.mock(KubernetesCluster.class); + Mockito.when(cluster.getId()).thenReturn(1L); + Mockito.when(cluster.getName()).thenReturn("test-cluster"); + + Long newNodeId1 = 100L; + Long newNodeId2 = 101L; + Long sharedHostId = 1000L; + + AffinityGroupVO affinityGroup = Mockito.mock(AffinityGroupVO.class); + Mockito.when(affinityGroup.getType()).thenReturn("host anti-affinity"); + Mockito.when(affinityGroup.getName()).thenReturn("anti-affinity-group"); + + VMInstanceVO newNode1 = Mockito.mock(VMInstanceVO.class); + Mockito.when(newNode1.getHostId()).thenReturn(sharedHostId); + Mockito.when(newNode1.getInstanceName()).thenReturn("new-node-vm-1"); + + VMInstanceVO newNode2 = Mockito.mock(VMInstanceVO.class); + Mockito.when(newNode2.getHostId()).thenReturn(sharedHostId); + Mockito.when(newNode2.getInstanceName()).thenReturn("new-node-vm-2"); + + HostVO host = Mockito.mock(HostVO.class); + Mockito.when(host.getName()).thenReturn("host-1"); + + Mockito.when(kubernetesClusterAffinityGroupMapDao.listAffinityGroupIdsByClusterIdAndNodeType(1L, WORKER.name())) + .thenReturn(Arrays.asList(10L)); + Mockito.when(affinityGroupDao.findById(10L)).thenReturn(affinityGroup); + Mockito.when(kubernetesClusterVmMapDao.listByClusterIdAndVmType(1L, WORKER)) + .thenReturn(Collections.emptyList()); + Mockito.when(vmInstanceDao.findById(newNodeId1)).thenReturn(newNode1); + Mockito.when(vmInstanceDao.findById(newNodeId2)).thenReturn(newNode2); + Mockito.when(hostDao.findById(sharedHostId)).thenReturn(host); + + kubernetesClusterManager.validateNodeAffinityGroups(Arrays.asList(newNodeId1, newNodeId2), cluster); + } + + @Test + public void testValidateNodeAffinityGroupsAntiAffinityMultipleNewNodesOnDifferentHosts() { + KubernetesCluster cluster = Mockito.mock(KubernetesCluster.class); + Mockito.when(cluster.getId()).thenReturn(1L); + Mockito.when(cluster.getName()).thenReturn("test-cluster"); + + Long newNodeId1 = 100L; + Long newNodeId2 = 101L; + Long hostId1 = 1000L; + Long hostId2 = 1001L; + + AffinityGroupVO affinityGroup = Mockito.mock(AffinityGroupVO.class); + Mockito.when(affinityGroup.getType()).thenReturn("host anti-affinity"); + Mockito.when(affinityGroup.getName()).thenReturn("anti-affinity-group"); + + VMInstanceVO newNode1 = Mockito.mock(VMInstanceVO.class); + Mockito.when(newNode1.getHostId()).thenReturn(hostId1); + + VMInstanceVO newNode2 = Mockito.mock(VMInstanceVO.class); + Mockito.when(newNode2.getHostId()).thenReturn(hostId2); + + Mockito.when(kubernetesClusterAffinityGroupMapDao.listAffinityGroupIdsByClusterIdAndNodeType(1L, WORKER.name())) + .thenReturn(Arrays.asList(10L)); + Mockito.when(affinityGroupDao.findById(10L)).thenReturn(affinityGroup); + Mockito.when(kubernetesClusterVmMapDao.listByClusterIdAndVmType(1L, WORKER)) + .thenReturn(Collections.emptyList()); + Mockito.when(vmInstanceDao.findById(newNodeId1)).thenReturn(newNode1); + Mockito.when(vmInstanceDao.findById(newNodeId2)).thenReturn(newNode2); + + kubernetesClusterManager.validateNodeAffinityGroups(Arrays.asList(newNodeId1, newNodeId2), cluster); + + Mockito.verify(kubernetesClusterAffinityGroupMapDao).listAffinityGroupIdsByClusterIdAndNodeType(1L, WORKER.name()); + } + + @Test + public void testValidateNodeAffinityGroupsNodeWithNullHost() { + KubernetesCluster cluster = Mockito.mock(KubernetesCluster.class); + Mockito.when(cluster.getId()).thenReturn(1L); + Mockito.when(cluster.getName()).thenReturn("test-cluster"); + + Long newNodeId = 100L; + + AffinityGroupVO affinityGroup = Mockito.mock(AffinityGroupVO.class); + Mockito.when(affinityGroup.getType()).thenReturn("host anti-affinity"); + Mockito.when(affinityGroup.getName()).thenReturn("anti-affinity-group"); + + VMInstanceVO newNode = Mockito.mock(VMInstanceVO.class); + Mockito.when(newNode.getHostId()).thenReturn(null); + + Mockito.when(kubernetesClusterAffinityGroupMapDao.listAffinityGroupIdsByClusterIdAndNodeType(1L, WORKER.name())) + .thenReturn(Arrays.asList(10L)); + Mockito.when(affinityGroupDao.findById(10L)).thenReturn(affinityGroup); + Mockito.when(kubernetesClusterVmMapDao.listByClusterIdAndVmType(1L, WORKER)) + .thenReturn(Collections.emptyList()); + Mockito.when(vmInstanceDao.findById(newNodeId)).thenReturn(newNode); + + kubernetesClusterManager.validateNodeAffinityGroups(Arrays.asList(newNodeId), cluster); + + Mockito.verify(vmInstanceDao).findById(newNodeId); + } + + @Test + public void testValidateNodeAffinityGroupsNullNode() { + KubernetesCluster cluster = Mockito.mock(KubernetesCluster.class); + Mockito.when(cluster.getId()).thenReturn(1L); + Mockito.when(cluster.getName()).thenReturn("test-cluster"); + + Long newNodeId = 100L; + + AffinityGroupVO affinityGroup = Mockito.mock(AffinityGroupVO.class); + Mockito.when(affinityGroup.getType()).thenReturn("host anti-affinity"); + Mockito.when(affinityGroup.getName()).thenReturn("anti-affinity-group"); + + Mockito.when(kubernetesClusterAffinityGroupMapDao.listAffinityGroupIdsByClusterIdAndNodeType(1L, WORKER.name())) + .thenReturn(Arrays.asList(10L)); + Mockito.when(affinityGroupDao.findById(10L)).thenReturn(affinityGroup); + Mockito.when(kubernetesClusterVmMapDao.listByClusterIdAndVmType(1L, WORKER)) + .thenReturn(Collections.emptyList()); + Mockito.when(vmInstanceDao.findById(newNodeId)).thenReturn(null); + + kubernetesClusterManager.validateNodeAffinityGroups(Arrays.asList(newNodeId), cluster); + + Mockito.verify(vmInstanceDao).findById(newNodeId); + } + + @Test + public void testValidateNodeAffinityGroupsAffinityNoExistingWorkers() { + KubernetesCluster cluster = Mockito.mock(KubernetesCluster.class); + Mockito.when(cluster.getId()).thenReturn(1L); + Mockito.when(cluster.getName()).thenReturn("test-cluster"); + + Long newNodeId = 100L; + Long newNodeHostId = 1000L; + + AffinityGroupVO affinityGroup = Mockito.mock(AffinityGroupVO.class); + Mockito.when(affinityGroup.getType()).thenReturn("host affinity"); + Mockito.when(affinityGroup.getName()).thenReturn("affinity-group"); + + VMInstanceVO newNode = Mockito.mock(VMInstanceVO.class); + Mockito.when(newNode.getHostId()).thenReturn(newNodeHostId); + + Mockito.when(kubernetesClusterAffinityGroupMapDao.listAffinityGroupIdsByClusterIdAndNodeType(1L, WORKER.name())) + .thenReturn(Arrays.asList(10L)); + Mockito.when(affinityGroupDao.findById(10L)).thenReturn(affinityGroup); + Mockito.when(kubernetesClusterVmMapDao.listByClusterIdAndVmType(1L, WORKER)) + .thenReturn(Collections.emptyList()); + Mockito.when(vmInstanceDao.findById(newNodeId)).thenReturn(newNode); + + kubernetesClusterManager.validateNodeAffinityGroups(Arrays.asList(newNodeId), cluster); + + Mockito.verify(kubernetesClusterAffinityGroupMapDao).listAffinityGroupIdsByClusterIdAndNodeType(1L, WORKER.name()); + } + } From d62b9f3b730d46651f31ba1ac80abeca1a48c2e8 Mon Sep 17 00:00:00 2001 From: Daman Arora Date: Mon, 12 Jan 2026 09:19:33 -0500 Subject: [PATCH 26/26] refactor test mocks to use lenient behavior --- .../KubernetesClusterManagerImplTest.java | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) 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 index ee4d6429e12d..71949459c865 100644 --- 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 @@ -651,7 +651,7 @@ public void testValidateNodeAffinityGroupsAntiAffinityNewNodeOnExistingHost() { public void testValidateNodeAffinityGroupsAntiAffinityNewNodeOnDifferentHost() { KubernetesCluster cluster = Mockito.mock(KubernetesCluster.class); Mockito.when(cluster.getId()).thenReturn(1L); - Mockito.when(cluster.getName()).thenReturn("test-cluster"); + Mockito.lenient().when(cluster.getName()).thenReturn("test-cluster"); Long newNodeId = 100L; Long existingWorkerVmId = 200L; @@ -660,7 +660,7 @@ public void testValidateNodeAffinityGroupsAntiAffinityNewNodeOnDifferentHost() { AffinityGroupVO affinityGroup = Mockito.mock(AffinityGroupVO.class); Mockito.when(affinityGroup.getType()).thenReturn("host anti-affinity"); - Mockito.when(affinityGroup.getName()).thenReturn("anti-affinity-group"); + Mockito.lenient().when(affinityGroup.getName()).thenReturn("anti-affinity-group"); VMInstanceVO newNode = Mockito.mock(VMInstanceVO.class); Mockito.when(newNode.getHostId()).thenReturn(newNodeHostId); @@ -688,7 +688,7 @@ public void testValidateNodeAffinityGroupsAntiAffinityNewNodeOnDifferentHost() { public void testValidateNodeAffinityGroupsAffinityNewNodeOnSameHost() { KubernetesCluster cluster = Mockito.mock(KubernetesCluster.class); Mockito.when(cluster.getId()).thenReturn(1L); - Mockito.when(cluster.getName()).thenReturn("test-cluster"); + Mockito.lenient().when(cluster.getName()).thenReturn("test-cluster"); Long newNodeId = 100L; Long existingWorkerVmId = 200L; @@ -696,7 +696,7 @@ public void testValidateNodeAffinityGroupsAffinityNewNodeOnSameHost() { AffinityGroupVO affinityGroup = Mockito.mock(AffinityGroupVO.class); Mockito.when(affinityGroup.getType()).thenReturn("host affinity"); - Mockito.when(affinityGroup.getName()).thenReturn("affinity-group"); + Mockito.lenient().when(affinityGroup.getName()).thenReturn("affinity-group"); VMInstanceVO newNode = Mockito.mock(VMInstanceVO.class); Mockito.when(newNode.getHostId()).thenReturn(sharedHostId); @@ -775,12 +775,12 @@ public void testValidateNodeAffinityGroupsAntiAffinityMultipleNewNodesOnSameHost Long sharedHostId = 1000L; AffinityGroupVO affinityGroup = Mockito.mock(AffinityGroupVO.class); - Mockito.when(affinityGroup.getType()).thenReturn("host anti-affinity"); + Mockito.lenient().when(affinityGroup.getType()).thenReturn("host anti-affinity"); Mockito.when(affinityGroup.getName()).thenReturn("anti-affinity-group"); VMInstanceVO newNode1 = Mockito.mock(VMInstanceVO.class); Mockito.when(newNode1.getHostId()).thenReturn(sharedHostId); - Mockito.when(newNode1.getInstanceName()).thenReturn("new-node-vm-1"); + Mockito.lenient().when(newNode1.getInstanceName()).thenReturn("new-node-vm-1"); VMInstanceVO newNode2 = Mockito.mock(VMInstanceVO.class); Mockito.when(newNode2.getHostId()).thenReturn(sharedHostId); @@ -805,7 +805,7 @@ public void testValidateNodeAffinityGroupsAntiAffinityMultipleNewNodesOnSameHost public void testValidateNodeAffinityGroupsAntiAffinityMultipleNewNodesOnDifferentHosts() { KubernetesCluster cluster = Mockito.mock(KubernetesCluster.class); Mockito.when(cluster.getId()).thenReturn(1L); - Mockito.when(cluster.getName()).thenReturn("test-cluster"); + Mockito.lenient().when(cluster.getName()).thenReturn("test-cluster"); Long newNodeId1 = 100L; Long newNodeId2 = 101L; @@ -813,8 +813,8 @@ public void testValidateNodeAffinityGroupsAntiAffinityMultipleNewNodesOnDifferen Long hostId2 = 1001L; AffinityGroupVO affinityGroup = Mockito.mock(AffinityGroupVO.class); - Mockito.when(affinityGroup.getType()).thenReturn("host anti-affinity"); - Mockito.when(affinityGroup.getName()).thenReturn("anti-affinity-group"); + Mockito.lenient().when(affinityGroup.getType()).thenReturn("host anti-affinity"); + Mockito.lenient().when(affinityGroup.getName()).thenReturn("anti-affinity-group"); VMInstanceVO newNode1 = Mockito.mock(VMInstanceVO.class); Mockito.when(newNode1.getHostId()).thenReturn(hostId1); @@ -839,13 +839,13 @@ public void testValidateNodeAffinityGroupsAntiAffinityMultipleNewNodesOnDifferen public void testValidateNodeAffinityGroupsNodeWithNullHost() { KubernetesCluster cluster = Mockito.mock(KubernetesCluster.class); Mockito.when(cluster.getId()).thenReturn(1L); - Mockito.when(cluster.getName()).thenReturn("test-cluster"); + Mockito.lenient().when(cluster.getName()).thenReturn("test-cluster"); Long newNodeId = 100L; AffinityGroupVO affinityGroup = Mockito.mock(AffinityGroupVO.class); - Mockito.when(affinityGroup.getType()).thenReturn("host anti-affinity"); - Mockito.when(affinityGroup.getName()).thenReturn("anti-affinity-group"); + Mockito.lenient().when(affinityGroup.getType()).thenReturn("host anti-affinity"); + Mockito.lenient().when(affinityGroup.getName()).thenReturn("anti-affinity-group"); VMInstanceVO newNode = Mockito.mock(VMInstanceVO.class); Mockito.when(newNode.getHostId()).thenReturn(null); @@ -859,20 +859,20 @@ public void testValidateNodeAffinityGroupsNodeWithNullHost() { kubernetesClusterManager.validateNodeAffinityGroups(Arrays.asList(newNodeId), cluster); - Mockito.verify(vmInstanceDao).findById(newNodeId); + Mockito.verify(vmInstanceDao, Mockito.atLeastOnce()).findById(newNodeId); } @Test public void testValidateNodeAffinityGroupsNullNode() { KubernetesCluster cluster = Mockito.mock(KubernetesCluster.class); Mockito.when(cluster.getId()).thenReturn(1L); - Mockito.when(cluster.getName()).thenReturn("test-cluster"); + Mockito.lenient().when(cluster.getName()).thenReturn("test-cluster"); Long newNodeId = 100L; AffinityGroupVO affinityGroup = Mockito.mock(AffinityGroupVO.class); - Mockito.when(affinityGroup.getType()).thenReturn("host anti-affinity"); - Mockito.when(affinityGroup.getName()).thenReturn("anti-affinity-group"); + Mockito.lenient().when(affinityGroup.getType()).thenReturn("host anti-affinity"); + Mockito.lenient().when(affinityGroup.getName()).thenReturn("anti-affinity-group"); Mockito.when(kubernetesClusterAffinityGroupMapDao.listAffinityGroupIdsByClusterIdAndNodeType(1L, WORKER.name())) .thenReturn(Arrays.asList(10L)); @@ -883,21 +883,21 @@ public void testValidateNodeAffinityGroupsNullNode() { kubernetesClusterManager.validateNodeAffinityGroups(Arrays.asList(newNodeId), cluster); - Mockito.verify(vmInstanceDao).findById(newNodeId); + Mockito.verify(vmInstanceDao, Mockito.atLeastOnce()).findById(newNodeId); } @Test public void testValidateNodeAffinityGroupsAffinityNoExistingWorkers() { KubernetesCluster cluster = Mockito.mock(KubernetesCluster.class); Mockito.when(cluster.getId()).thenReturn(1L); - Mockito.when(cluster.getName()).thenReturn("test-cluster"); + Mockito.lenient().when(cluster.getName()).thenReturn("test-cluster"); Long newNodeId = 100L; Long newNodeHostId = 1000L; AffinityGroupVO affinityGroup = Mockito.mock(AffinityGroupVO.class); - Mockito.when(affinityGroup.getType()).thenReturn("host affinity"); - Mockito.when(affinityGroup.getName()).thenReturn("affinity-group"); + Mockito.lenient().when(affinityGroup.getType()).thenReturn("host affinity"); + Mockito.lenient().when(affinityGroup.getName()).thenReturn("affinity-group"); VMInstanceVO newNode = Mockito.mock(VMInstanceVO.class); Mockito.when(newNode.getHostId()).thenReturn(newNodeHostId);