From fcdb805adf14409bb5d6f9c51e7d7bd51506d8ed Mon Sep 17 00:00:00 2001 From: Brad House Date: Mon, 9 Nov 2026 19:03:38 +0000 Subject: [PATCH] CKS: deploy nodes of a cluster created before 4.21 from the default template Clusters created before 4.21 have no control, worker or etcd node template set: those columns were added by the 4.21 upgrade and left NULL. The action workers looked the node templates up from those columns only, so scaling up such a cluster passed a null template to the VM deployment, which failed with a NullPointerException. A node type with no template of its own now uses the template CKS picks by default, as these clusters did before 4.21. Such a cluster also counts as using the default template, so the owner gets launch permission for it while nodes are deployed, as before. Signed-off-by: Brad House --- .../KubernetesClusterActionWorker.java | 18 ++- ...esClusterActionWorkerNodeTemplateTest.java | 116 ++++++++++++++++++ 2 files changed, 131 insertions(+), 3 deletions(-) create mode 100644 plugins/integrations/kubernetes-service/src/test/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterActionWorkerNodeTemplateTest.java 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 7a06b11e3bf4..17177983f230 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 @@ -286,12 +286,20 @@ protected void init() { Hypervisor.HypervisorType type = template.getHypervisorType(); KubernetesSupportedVersionVO kubernetesSupportedVersion = kubernetesSupportedVersionDao.findById(this.kubernetesCluster.getKubernetesVersionId()); this.clusterTemplate = manager.getKubernetesServiceTemplate(dataCenterVO, type, null, KubernetesClusterNodeType.DEFAULT, kubernetesSupportedVersion); - this.controlNodeTemplate = templateDao.findById(this.kubernetesCluster.getControlNodeTemplateId()); - this.workerNodeTemplate = templateDao.findById(this.kubernetesCluster.getWorkerNodeTemplateId()); - this.etcdTemplate = templateDao.findById(this.kubernetesCluster.getEtcdNodeTemplateId()); + this.controlNodeTemplate = getNodeTypeTemplate(this.kubernetesCluster.getControlNodeTemplateId()); + this.workerNodeTemplate = getNodeTypeTemplate(this.kubernetesCluster.getWorkerNodeTemplateId()); + this.etcdTemplate = getNodeTypeTemplate(this.kubernetesCluster.getEtcdNodeTemplateId()); this.sshKeyFile = getManagementServerSshPublicKeyFile(); } + /** + * A cluster created before 4.21 has no template set for its node types. Its nodes are deployed from the default + * template, as they were before. + */ + protected VirtualMachineTemplate getNodeTypeTemplate(Long nodeTypeTemplateId) { + return nodeTypeTemplateId != null ? templateDao.findById(nodeTypeTemplateId) : clusterTemplate; + } + protected String readResourceFile(String resource) throws IOException { return IOUtils.toString(Objects.requireNonNull(Thread.currentThread().getContextClassLoader().getResourceAsStream(resource)), com.cloud.utils.StringUtils.getPreferredCharset()); } @@ -985,6 +993,10 @@ protected ServiceOffering getServiceOfferingForNodeTypeOnCluster(KubernetesClust } protected boolean isDefaultTemplateUsed() { + if (kubernetesCluster.getControlNodeTemplateId() == null || kubernetesCluster.getWorkerNodeTemplateId() == null) { + // created before 4.21, see getNodeTypeTemplate() + return true; + } if (Arrays.asList(kubernetesCluster.getControlNodeTemplateId(), kubernetesCluster.getWorkerNodeTemplateId(), kubernetesCluster.getEtcdNodeTemplateId()).contains(kubernetesCluster.getTemplateId())) { return true; } diff --git a/plugins/integrations/kubernetes-service/src/test/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterActionWorkerNodeTemplateTest.java b/plugins/integrations/kubernetes-service/src/test/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterActionWorkerNodeTemplateTest.java new file mode 100644 index 000000000000..aacb6a0027e8 --- /dev/null +++ b/plugins/integrations/kubernetes-service/src/test/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterActionWorkerNodeTemplateTest.java @@ -0,0 +1,116 @@ +// 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.actionworkers; + +import org.apache.cloudstack.framework.config.dao.ConfigurationDao; +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 com.cloud.dc.dao.DataCenterDao; +import com.cloud.kubernetes.cluster.KubernetesCluster; +import com.cloud.kubernetes.cluster.KubernetesClusterManagerImpl; +import com.cloud.kubernetes.cluster.KubernetesServiceHelper.KubernetesClusterNodeType; +import com.cloud.kubernetes.version.dao.KubernetesSupportedVersionDao; +import com.cloud.storage.VMTemplateVO; +import com.cloud.storage.dao.VMTemplateDao; +import com.cloud.user.dao.AccountDao; + +/** + * The node templates of a cluster, including one created before its node types had templates of their own. + */ +@RunWith(MockitoJUnitRunner.class) +public class KubernetesClusterActionWorkerNodeTemplateTest { + + private static final long CLUSTER_TEMPLATE_ID = 3L; + + @Mock + KubernetesClusterManagerImpl kubernetesClusterManager; + + @Mock + KubernetesCluster kubernetesCluster; + + @Mock + VMTemplateDao templateDao; + + @Mock + VMTemplateVO defaultTemplate; + + KubernetesClusterActionWorker actionWorker; + + @Before + public void setUp() { + actionWorker = new KubernetesClusterActionWorker(kubernetesCluster, kubernetesClusterManager); + actionWorker.templateDao = templateDao; + actionWorker.accountDao = Mockito.mock(AccountDao.class); + actionWorker.dataCenterDao = Mockito.mock(DataCenterDao.class); + actionWorker.configurationDao = Mockito.mock(ConfigurationDao.class); + actionWorker.kubernetesSupportedVersionDao = Mockito.mock(KubernetesSupportedVersionDao.class); + Mockito.lenient().when(templateDao.findById(CLUSTER_TEMPLATE_ID)).thenReturn(Mockito.mock(VMTemplateVO.class)); + Mockito.lenient().when(kubernetesClusterManager.getKubernetesServiceTemplate(Mockito.any(), Mockito.any(), Mockito.isNull(), + Mockito.eq(KubernetesClusterNodeType.DEFAULT), Mockito.any())).thenReturn(defaultTemplate); + } + + private void mockClusterTemplates(Long controlTemplateId, Long workerTemplateId, Long etcdTemplateId) { + Mockito.lenient().when(kubernetesCluster.getTemplateId()).thenReturn(CLUSTER_TEMPLATE_ID); + Mockito.lenient().when(kubernetesCluster.getControlNodeTemplateId()).thenReturn(controlTemplateId); + Mockito.lenient().when(kubernetesCluster.getWorkerNodeTemplateId()).thenReturn(workerTemplateId); + Mockito.lenient().when(kubernetesCluster.getEtcdNodeTemplateId()).thenReturn(etcdTemplateId); + } + + @Test + public void testInitClusterWithoutNodeTypeTemplates() { + // a cluster created before 4.21 + mockClusterTemplates(null, null, null); + + actionWorker.init(); + + Assert.assertSame(defaultTemplate, actionWorker.controlNodeTemplate); + Assert.assertSame(defaultTemplate, actionWorker.workerNodeTemplate); + Assert.assertSame(defaultTemplate, actionWorker.etcdTemplate); + Assert.assertTrue(actionWorker.isDefaultTemplateUsed()); + } + + @Test + public void testInitClusterWithNodeTypeTemplates() { + mockClusterTemplates(5L, 6L, 7L); + VMTemplateVO controlTemplate = Mockito.mock(VMTemplateVO.class); + VMTemplateVO workerTemplate = Mockito.mock(VMTemplateVO.class); + VMTemplateVO etcdTemplate = Mockito.mock(VMTemplateVO.class); + Mockito.when(templateDao.findById(5L)).thenReturn(controlTemplate); + Mockito.when(templateDao.findById(6L)).thenReturn(workerTemplate); + Mockito.when(templateDao.findById(7L)).thenReturn(etcdTemplate); + + actionWorker.init(); + + Assert.assertSame(defaultTemplate, actionWorker.clusterTemplate); + Assert.assertSame(controlTemplate, actionWorker.controlNodeTemplate); + Assert.assertSame(workerTemplate, actionWorker.workerNodeTemplate); + Assert.assertSame(etcdTemplate, actionWorker.etcdTemplate); + Assert.assertFalse(actionWorker.isDefaultTemplateUsed()); + } + + @Test + public void testIsDefaultTemplateUsedByANodeType() { + mockClusterTemplates(5L, CLUSTER_TEMPLATE_ID, null); + Assert.assertTrue(actionWorker.isDefaultTemplateUsed()); + } +}