This is an automated email from the ASF dual-hosted git repository. pearl11594 pushed a commit to branch fr06-cks-template-register in repository https://gitbox.apache.org/repos/asf/cloudstack.git
commit 959f2b6a30bfd8bf4e0ae20a4b6ca63895f7a63f Author: nvazquez <nicovazque...@gmail.com> AuthorDate: Wed Jan 31 15:35:37 2024 -0300 Refactor and add UI for createKubernetesCluster API advanced settings --- .../cluster/KubernetesClusterHelper.java | 2 +- .../org/apache/cloudstack/api/ApiConstants.java | 3 +- .../cluster/CreateKubernetesClusterCmd.java | 42 +++++++++- .../cluster/CreateKubernetesClusterCmdTest.java | 55 +++++++++--- ui/public/locales/en.json | 4 + ui/src/views/compute/CreateKubernetesCluster.vue | 97 ++++++++++++++++++++++ 6 files changed, 190 insertions(+), 13 deletions(-) diff --git a/api/src/main/java/com/cloud/kubernetes/cluster/KubernetesClusterHelper.java b/api/src/main/java/com/cloud/kubernetes/cluster/KubernetesClusterHelper.java index 896bdcfde57..4b61fe5ad03 100644 --- a/api/src/main/java/com/cloud/kubernetes/cluster/KubernetesClusterHelper.java +++ b/api/src/main/java/com/cloud/kubernetes/cluster/KubernetesClusterHelper.java @@ -22,7 +22,7 @@ import org.apache.cloudstack.acl.ControlledEntity; public interface KubernetesClusterHelper extends Adapter { enum KubernetesClusterNodeType { - WORKER, MASTER, ETCD + CONTROL, WORKER, ETCD } ControlledEntity findByUuid(String uuid); 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 ef21916c96d..350d8742fb2 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -1028,6 +1028,7 @@ public class ApiConstants { public static final String MASTER_NODES = "masternodes"; public static final String NODE_IDS = "nodeids"; public static final String CONTROL_NODES = "controlnodes"; + public static final String ETCD_NODES = "etcdnodes"; public static final String MIN_SEMANTIC_VERSION = "minimumsemanticversion"; public static final String MIN_KUBERNETES_VERSION_ID = "minimumkubernetesversionid"; public static final String NODE_ROOT_DISK_SIZE = "noderootdisksize"; @@ -1036,7 +1037,7 @@ public class ApiConstants { public static final String AUTOSCALING_ENABLED = "autoscalingenabled"; public static final String MIN_SIZE = "minsize"; public static final String MAX_SIZE = "maxsize"; - public static final String NODE_TYPE_OFFERING_MAP = "nodetypeofferingmap"; + public static final String NODE_TYPE_OFFERING_MAP = "nodeofferings"; 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 9b4067ce0a2..99943408fc6 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,7 +17,9 @@ package org.apache.cloudstack.api.command.user.kubernetes.cluster; import java.security.InvalidParameterException; +import java.util.Arrays; import java.util.HashMap; +import java.util.List; import java.util.Map; import javax.inject.Inject; @@ -94,13 +96,19 @@ public class CreateKubernetesClusterCmd extends BaseAsyncCreateCmd { @ACL(accessType = AccessType.UseEntry) @Parameter(name = ApiConstants.SERVICE_OFFERING_ID, type = CommandType.UUID, entityType = ServiceOfferingResponse.class, description = "the ID of the service offering for the virtual machines in the cluster.") - private Long serviceOfferingId; + protected Long serviceOfferingId; @ACL(accessType = AccessType.UseEntry) @Parameter(name = ApiConstants.NODE_TYPE_OFFERING_MAP, type = CommandType.MAP, description = "(Optional) Node Type to Service Offering ID mapping. If provided, it overrides the serviceofferingid parameter") protected Map<String, Map<String, String>> nodeTypeOfferingMap; + @ACL(accessType = AccessType.UseEntry) + @Parameter(name = ApiConstants.ETCD_NODES, type = CommandType.LONG, + description = "(Optional) Number of Kubernetes cluster etcd nodes, default is 0." + + "In case the number is greater than 0, etcd nodes are separate from master nodes and are provisioned accordingly") + protected Long etcdNodes; + @ACL(accessType = AccessType.UseEntry) @Parameter(name = ApiConstants.ACCOUNT, type = CommandType.STRING, description = "an optional account for the" + " virtual machine. Must be used with domainId.") @@ -220,6 +228,10 @@ public class CreateKubernetesClusterCmd extends BaseAsyncCreateCmd { return controlNodes; } + public Long getEtcdNodes() { + return etcdNodes == null ? 0L : etcdNodes; + } + public String getExternalLoadBalancerIpAddress() { return externalLoadBalancerIpAddress; } @@ -307,10 +319,38 @@ public class CreateKubernetesClusterCmd extends BaseAsyncCreateCmd { for (Map<String, String> entry : nodeTypeOfferingMap.values()) { processNodeTypeOfferingEntryAndAddToMappingIfValid(entry, mapping); } + addMissingNodeTypeDefaultOffering(mapping, serviceOfferingId, etcdNodes); + } else { + addDefaultNodeTypeOfferingEntries(serviceOfferingId, etcdNodes, mapping); } return mapping; } + private void addMissingNodeTypeDefaultOffering(Map<String, Long> mapping, Long serviceOfferingId, Long etcdNodes) { + if (MapUtils.isEmpty(mapping)) { + return; + } + boolean addEtcdOffering = etcdNodes != null && etcdNodes > 0; + List<String> keys = Arrays.asList(KubernetesClusterNodeType.CONTROL.name(), KubernetesClusterNodeType.WORKER.name(), KubernetesClusterNodeType.ETCD.name()); + for (String key : keys) { + if (mapping.containsKey(key)) { + continue; + } + if (!key.equalsIgnoreCase(KubernetesClusterNodeType.ETCD.name()) || + (addEtcdOffering && key.equalsIgnoreCase(KubernetesClusterNodeType.ETCD.name()))) { + mapping.put(key, serviceOfferingId); + } + } + } + + protected void addDefaultNodeTypeOfferingEntries(Long serviceOfferingId, Long etcdNodes, Map<String, Long> mapping) { + mapping.put(KubernetesClusterNodeType.CONTROL.name(), serviceOfferingId); + mapping.put(KubernetesClusterNodeType.WORKER.name(), serviceOfferingId); + if (etcdNodes != null && etcdNodes > 0) { + mapping.put(KubernetesClusterNodeType.ETCD.name(), serviceOfferingId); + } + } + ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// diff --git a/plugins/integrations/kubernetes-service/src/test/java/org/apache/cloudstack/api/command/user/kubernetes/cluster/CreateKubernetesClusterCmdTest.java b/plugins/integrations/kubernetes-service/src/test/java/org/apache/cloudstack/api/command/user/kubernetes/cluster/CreateKubernetesClusterCmdTest.java index 25dbfae4d86..406516581a7 100644 --- a/plugins/integrations/kubernetes-service/src/test/java/org/apache/cloudstack/api/command/user/kubernetes/cluster/CreateKubernetesClusterCmdTest.java +++ b/plugins/integrations/kubernetes-service/src/test/java/org/apache/cloudstack/api/command/user/kubernetes/cluster/CreateKubernetesClusterCmdTest.java @@ -34,7 +34,8 @@ import java.util.HashMap; import java.util.Map; import java.util.UUID; -import static com.cloud.kubernetes.cluster.KubernetesClusterHelper.KubernetesClusterNodeType.MASTER; +import static com.cloud.kubernetes.cluster.KubernetesClusterHelper.KubernetesClusterNodeType.CONTROL; +import static com.cloud.kubernetes.cluster.KubernetesClusterHelper.KubernetesClusterNodeType.ETCD; import static com.cloud.kubernetes.cluster.KubernetesClusterHelper.KubernetesClusterNodeType.WORKER; @RunWith(MockitoJUnitRunner.class) @@ -47,23 +48,29 @@ public class CreateKubernetesClusterCmdTest { @Mock ServiceOffering workerServiceOffering; @Mock - ServiceOffering masterServiceOffering; + ServiceOffering controlServiceOffering; + @Mock + ServiceOffering etcdServiceOffering; private final CreateKubernetesClusterCmd cmd = new CreateKubernetesClusterCmd(); private static final String workerNodesOfferingId = UUID.randomUUID().toString(); - private static final String masterNodesOfferingId = 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 masterOfferingId = 2L; + private static final Long controlOfferingId = 2L; + private static final Long etcdOfferingId = 3L; @Before public void setUp() { cmd._entityMgr = entityManager; cmd.kubernetesClusterHelper = helper; Mockito.when(entityManager.findByUuid(ServiceOffering.class, workerNodesOfferingId)).thenReturn(workerServiceOffering); - Mockito.when(entityManager.findByUuid(ServiceOffering.class, masterNodesOfferingId)).thenReturn(masterServiceOffering); + Mockito.when(entityManager.findByUuid(ServiceOffering.class, controlNodesOfferingId)).thenReturn(controlServiceOffering); + Mockito.when(entityManager.findByUuid(ServiceOffering.class, etcdNodesOfferingId)).thenReturn(etcdServiceOffering); Mockito.when(workerServiceOffering.getId()).thenReturn(workerOfferingId); - Mockito.when(masterServiceOffering.getId()).thenReturn(masterOfferingId); + Mockito.when(controlServiceOffering.getId()).thenReturn(controlOfferingId); + Mockito.when(etcdServiceOffering.getId()).thenReturn(etcdOfferingId); } private Map<String, String> createMapEntry(KubernetesClusterHelper.KubernetesClusterNodeType nodeType, @@ -75,18 +82,46 @@ public class CreateKubernetesClusterCmdTest { } @Test - public void testNodeOfferingMap() { + public void testNodeOfferingMapMissingEtcd() { cmd.nodeTypeOfferingMap = new HashMap<>(); Map<String, String> firstMap = createMapEntry(WORKER, workerNodesOfferingId); - Map<String, String> secondMap = createMapEntry(MASTER, masterNodesOfferingId); + Map<String, String> secondMap = createMapEntry(CONTROL, controlNodesOfferingId); cmd.nodeTypeOfferingMap.put("map1", firstMap); cmd.nodeTypeOfferingMap.put("map2", secondMap); Map<String, Long> map = cmd.getNodeTypeOfferingMap(); Assert.assertNotNull(map); Assert.assertEquals(2, map.size()); - Assert.assertTrue(map.containsKey(WORKER.name()) && map.containsKey(MASTER.name())); + Assert.assertTrue(map.containsKey(WORKER.name()) && map.containsKey(CONTROL.name())); Assert.assertEquals(workerOfferingId, map.get(WORKER.name())); - Assert.assertEquals(masterOfferingId, map.get(MASTER.name())); + Assert.assertEquals(controlOfferingId, map.get(CONTROL.name())); + } + + @Test + public void testNodeOfferingMapNullMap() { + cmd.nodeTypeOfferingMap = null; + cmd.serviceOfferingId = controlOfferingId; + Map<String, Long> map = cmd.getNodeTypeOfferingMap(); + Assert.assertNotNull(map); + Assert.assertEquals(2, map.size()); + Assert.assertTrue(map.containsKey(WORKER.name()) && map.containsKey(CONTROL.name())); + Assert.assertEquals(controlOfferingId, map.get(WORKER.name())); + Assert.assertEquals(controlOfferingId, map.get(CONTROL.name())); + } + + @Test + public void testNodeOfferingMapEtcdNodes() { + cmd.nodeTypeOfferingMap = new HashMap<>(); + Map<String, String> firstMap = createMapEntry(ETCD, etcdNodesOfferingId); + cmd.nodeTypeOfferingMap.put("map1", firstMap); + cmd.etcdNodes = 2L; + cmd.serviceOfferingId = controlOfferingId; + Map<String, Long> map = cmd.getNodeTypeOfferingMap(); + Assert.assertNotNull(map); + Assert.assertEquals(3, map.size()); + Assert.assertTrue(map.containsKey(WORKER.name()) && map.containsKey(CONTROL.name()) && map.containsKey(ETCD.name())); + Assert.assertEquals(controlOfferingId, map.get(WORKER.name())); + Assert.assertEquals(controlOfferingId, map.get(CONTROL.name())); + Assert.assertEquals(etcdOfferingId, map.get(ETCD.name())); } @Test(expected = InvalidParameterValueException.class) diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json index 6098bec5346..2498d5688dc 100644 --- a/ui/public/locales/en.json +++ b/ui/public/locales/en.json @@ -451,9 +451,13 @@ "label.cisco.nexus1000v.password": "Nexus 1000v password", "label.cisco.nexus1000v.username": "Nexus 1000v username", "label.cks.cluster.autoscalingenabled": "Enable auto scaling on this cluster", +"label.cks.cluster.control.nodes.offeringid": "Service Offering for Control Nodes", +"label.cks.cluster.etcd.nodes": "Etcd Nodes", +"label.cks.cluster.etcd.nodes.offeringid": "Service Offering for etcd Nodes", "label.cks.cluster.maxsize": "Maximum cluster size (Worker nodes)", "label.cks.cluster.minsize": "Minimum cluster size (Worker nodes)", "label.cks.cluster.size": "Cluster size (Worker nodes)", +"label.cks.cluster.worker.nodes.offeringid": "Service Offering for Worker Nodes", "label.cleanup": "Clean up", "label.clear": "Clear", "label.clear.list": "Clear list", diff --git a/ui/src/views/compute/CreateKubernetesCluster.vue b/ui/src/views/compute/CreateKubernetesCluster.vue index ca1e424cca4..214b3022482 100644 --- a/ui/src/views/compute/CreateKubernetesCluster.vue +++ b/ui/src/views/compute/CreateKubernetesCluster.vue @@ -180,6 +180,81 @@ </a-select-option> </a-select> </a-form-item> + + <!-- Advanced configurations --> + <a-form-item name="advancedmode" ref="advancedmode"> + <template #label> + <tooltip-label :title="$t('label.isadvanced')" /> + </template> + <a-switch v-model:checked="form.advancedmode" /> + </a-form-item> + <a-form-item v-if="form.advancedmode" name="controlofferingid" ref="controlofferingid"> + <template #label> + <tooltip-label :title="$t('label.cks.cluster.control.nodes.offeringid')" :tooltip="$t('label.cks.cluster.control.nodes.offeringid')"/> + </template> + <a-select + id="control-offering-selection" + v-model:value="form.controlofferingid" + showSearch + optionFilterProp="label" + :filterOption="(input, option) => { + return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0 + }" + :loading="serviceOfferingLoading" + :placeholder="$t('label.cks.cluster.control.nodes.offeringid')"> + <a-select-option v-for="(opt, optIndex) in serviceOfferings" :key="optIndex" :label="opt.name || opt.description"> + {{ opt.name || opt.description }} + </a-select-option> + </a-select> + </a-form-item> + <a-form-item v-if="form.advancedmode" name="workerofferingid" ref="workerofferingid"> + <template #label> + <tooltip-label :title="$t('label.cks.cluster.worker.nodes.offeringid')" :tooltip="$t('label.cks.cluster.worker.nodes.offeringid')"/> + </template> + <a-select + id="worker-offering-selection" + v-model:value="form.workerofferingid" + showSearch + optionFilterProp="label" + :filterOption="(input, option) => { + return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0 + }" + :loading="serviceOfferingLoading" + :placeholder="$t('label.cks.cluster.worker.nodes.offeringid')"> + <a-select-option v-for="(opt, optIndex) in serviceOfferings" :key="optIndex" :label="opt.name || opt.description"> + {{ opt.name || opt.description }} + </a-select-option> + </a-select> + </a-form-item> + <a-form-item v-if="form.advancedmode" name="etcdnodes" ref="etcdnodes"> + <template #label> + <tooltip-label :title="$t('label.cks.cluster.etcd.nodes')" :tooltip="apiParams.controlnodes.description"/> + </template> + <a-input + v-model:value="form.etcdnodes" + :placeholder="apiParams.controlnodes.description"/> + </a-form-item> + <a-form-item v-if="form.advancedmode && form.etcdnodes && form.etcdnodes > 0" name="etcdofferingid" ref="etcdofferingid"> + <template #label> + <tooltip-label :title="$t('label.cks.cluster.etcd.nodes.offeringid')" :tooltip="$t('label.cks.cluster.etcd.nodes.offeringid')"/> + </template> + <a-select + id="etcd-offering-selection" + v-model:value="form.etcdofferingid" + showSearch + optionFilterProp="label" + :filterOption="(input, option) => { + return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0 + }" + :loading="serviceOfferingLoading" + :placeholder="$t('label.cks.cluster.etcd.nodes.offeringid')"> + <a-select-option v-for="(opt, optIndex) in serviceOfferings" :key="optIndex" :label="opt.name || opt.description"> + {{ opt.name || opt.description }} + </a-select-option> + </a-select> + </a-form-item> + + <!-- Experimentation Features --> <div v-if="$store.getters.features.kubernetesclusterexperimentalfeaturesenabled"> <a-form-item name="privateregistry" ref="privateregistry" :label="$t('label.private.registry')"> <template #label> @@ -401,6 +476,9 @@ export default { this.serviceOfferingLoading = false if (this.arrayHasItems(this.serviceOfferings)) { this.form.serviceofferingid = 0 + this.form.controlofferingid = undefined + this.form.workerofferingid = undefined + this.form.etcdofferingid = undefined } }) }, @@ -461,6 +539,25 @@ export default { size: values.size, clustertype: 'CloudManaged' } + var advancedOfferings = 0 + if (this.isValidValueForKey(values, 'advancedmode') && values.advancedmode && this.isValidValueForKey(values, 'controlofferingid') && this.arrayHasItems(this.serviceOfferings) && this.serviceOfferings[values.controlofferingid].id != null) { + params['nodeofferings[' + advancedOfferings + '].node'] = 'control' + params['nodeofferings[' + advancedOfferings + '].offering'] = this.serviceOfferings[values.controlofferingid].id + advancedOfferings++ + } + if (this.isValidValueForKey(values, 'advancedmode') && values.advancedmode && this.isValidValueForKey(values, 'workerofferingid') && this.arrayHasItems(this.serviceOfferings) && this.serviceOfferings[values.workerofferingid].id != null) { + params['nodeofferings[' + advancedOfferings + '].node'] = 'worker' + params['nodeofferings[' + advancedOfferings + '].offering'] = this.serviceOfferings[values.workerofferingid].id + advancedOfferings++ + } + if (this.isValidValueForKey(values, 'advancedmode') && values.advancedmode && this.isValidValueForKey(values, 'etcdnodes') && values.etcdnodes > 0) { + params.etcdnodes = values.etcdnodes + if (this.isValidValueForKey(values, 'etcdofferingid') && this.arrayHasItems(this.serviceOfferings) && this.serviceOfferings[values.etcdofferingid].id != null) { + params['nodeofferings[' + advancedOfferings + '].node'] = 'etcd' + params['nodeofferings[' + advancedOfferings + '].offering'] = this.serviceOfferings[values.etcdofferingid].id + advancedOfferings++ + } + } if (this.isValidValueForKey(values, 'noderootdisksize') && values.noderootdisksize > 0) { params.noderootdisksize = values.noderootdisksize }