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 60cd9a2dff4e..667bc00605b9 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 @@ -153,6 +153,8 @@ public class KubernetesClusterResourceModifierActionWorker extends KubernetesClu protected String kubernetesClusterNodeNamePrefix; + private static final int MAX_CLUSTER_PREFIX_LENGTH = 43; + protected KubernetesClusterResourceModifierActionWorker(final KubernetesCluster kubernetesCluster, final KubernetesClusterManagerImpl clusterManager) { super(kubernetesCluster, clusterManager); } @@ -772,19 +774,35 @@ protected void setupKubernetesClusterVpcTierRules(IpAddress publicIp, Network ne } } + /** + * Generates a valid name prefix for Kubernetes cluster nodes. + * + *

The prefix must comply with Kubernetes naming constraints: + *

+ * + *

The generated prefix is limited to 43 characters to accommodate the full node naming pattern: + *

{'prefix'}-{'control' | 'node'}-{'11-digit-hash'}
+ * + * @return A valid node name prefix, truncated if necessary + * @see Kubernetes "Object Names and IDs" documentation + */ protected String getKubernetesClusterNodeNamePrefix() { - String prefix = kubernetesCluster.getName(); - if (!NetUtils.verifyDomainNameLabel(prefix, true)) { - prefix = prefix.replaceAll("[^a-zA-Z0-9-]", ""); - if (prefix.length() == 0) { - prefix = kubernetesCluster.getUuid(); - } - prefix = "k8s-" + prefix; + String prefix = kubernetesCluster.getName().toLowerCase(); + + if (NetUtils.verifyDomainNameLabel(prefix, true)) { + return StringUtils.truncate(prefix, MAX_CLUSTER_PREFIX_LENGTH); } - if (prefix.length() > 40) { - prefix = prefix.substring(0, 40); + + prefix = prefix.replaceAll("[^a-z0-9-]", ""); + if (prefix.isEmpty()) { + prefix = kubernetesCluster.getUuid(); } - return prefix; + return StringUtils.truncate("k8s-" + prefix, MAX_CLUSTER_PREFIX_LENGTH); } protected KubernetesClusterVO updateKubernetesClusterEntry(final Long cores, final Long memory, final Long size, diff --git a/plugins/integrations/kubernetes-service/src/test/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterResourceModifierActionWorkerTest.java b/plugins/integrations/kubernetes-service/src/test/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterResourceModifierActionWorkerTest.java new file mode 100644 index 000000000000..c220a3468afb --- /dev/null +++ b/plugins/integrations/kubernetes-service/src/test/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterResourceModifierActionWorkerTest.java @@ -0,0 +1,138 @@ +// 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 com.cloud.kubernetes.cluster.KubernetesCluster; +import com.cloud.kubernetes.cluster.KubernetesClusterManagerImpl; +import com.cloud.kubernetes.cluster.dao.KubernetesClusterDao; +import com.cloud.kubernetes.cluster.dao.KubernetesClusterDetailsDao; +import com.cloud.kubernetes.cluster.dao.KubernetesClusterVmMapDao; +import com.cloud.kubernetes.version.dao.KubernetesSupportedVersionDao; +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; + +@RunWith(MockitoJUnitRunner.class) +public class KubernetesClusterResourceModifierActionWorkerTest { + @Mock + private KubernetesClusterDao kubernetesClusterDaoMock; + + @Mock + private KubernetesClusterDetailsDao kubernetesClusterDetailsDaoMock; + + @Mock + private KubernetesClusterVmMapDao kubernetesClusterVmMapDaoMock; + + @Mock + private KubernetesSupportedVersionDao kubernetesSupportedVersionDaoMock; + + @Mock + private KubernetesClusterManagerImpl kubernetesClusterManagerMock; + + @Mock + private KubernetesCluster kubernetesClusterMock; + + private KubernetesClusterResourceModifierActionWorker kubernetesClusterResourceModifierActionWorker; + + @Before + public void setUp() { + kubernetesClusterManagerMock.kubernetesClusterDao = kubernetesClusterDaoMock; + kubernetesClusterManagerMock.kubernetesSupportedVersionDao = kubernetesSupportedVersionDaoMock; + kubernetesClusterManagerMock.kubernetesClusterDetailsDao = kubernetesClusterDetailsDaoMock; + kubernetesClusterManagerMock.kubernetesClusterVmMapDao = kubernetesClusterVmMapDaoMock; + + kubernetesClusterResourceModifierActionWorker = new KubernetesClusterResourceModifierActionWorker(kubernetesClusterMock, kubernetesClusterManagerMock); + } + + @Test + public void getKubernetesClusterNodeNamePrefixTestReturnOriginalPrefixWhenNamingAllRequirementsAreMet() { + String originalPrefix = "k8s-cluster-01"; + String expectedPrefix = "k8s-cluster-01"; + + Mockito.when(kubernetesClusterMock.getName()).thenReturn(originalPrefix); + Assert.assertEquals(expectedPrefix, kubernetesClusterResourceModifierActionWorker.getKubernetesClusterNodeNamePrefix()); + } + + @Test + public void getKubernetesClusterNodeNamePrefixTestNormalizedPrefixShouldOnlyContainLowerCaseCharacters() { + String originalPrefix = "k8s-CLUSTER-01"; + String expectedPrefix = "k8s-cluster-01"; + + Mockito.when(kubernetesClusterMock.getName()).thenReturn(originalPrefix); + Assert.assertEquals(expectedPrefix, kubernetesClusterResourceModifierActionWorker.getKubernetesClusterNodeNamePrefix()); + } + + @Test + public void getKubernetesClusterNodeNamePrefixTestNormalizedPrefixShouldBeTruncatedWhenRequired() { + int maxPrefixLength = 43; + + String originalPrefix = "c".repeat(maxPrefixLength + 1); + String expectedPrefix = "c".repeat(maxPrefixLength); + + Mockito.when(kubernetesClusterMock.getName()).thenReturn(originalPrefix); + String normalizedPrefix = kubernetesClusterResourceModifierActionWorker.getKubernetesClusterNodeNamePrefix(); + Assert.assertEquals(expectedPrefix, normalizedPrefix); + Assert.assertEquals(maxPrefixLength, normalizedPrefix.length()); + } + + @Test + public void getKubernetesClusterNodeNamePrefixTestNormalizedPrefixShouldBeTruncatedWhenRequiredAndWhenOriginalPrefixIsInvalid() { + int maxPrefixLength = 43; + + String originalPrefix = "1!" + "c".repeat(maxPrefixLength); + String expectedPrefix = "k8s-1" + "c".repeat(maxPrefixLength - 5); + + Mockito.when(kubernetesClusterMock.getName()).thenReturn(originalPrefix); + String normalizedPrefix = kubernetesClusterResourceModifierActionWorker.getKubernetesClusterNodeNamePrefix(); + Assert.assertEquals(expectedPrefix, normalizedPrefix); + Assert.assertEquals(maxPrefixLength, normalizedPrefix.length()); + } + + @Test + public void getKubernetesClusterNodeNamePrefixTestNormalizedPrefixShouldOnlyIncludeAlphanumericCharactersAndHyphen() { + String originalPrefix = "Cluster!@#$%^&*()_+?.-01|<>"; + String expectedPrefix = "k8s-cluster-01"; + + Mockito.when(kubernetesClusterMock.getName()).thenReturn(originalPrefix); + Assert.assertEquals(expectedPrefix, kubernetesClusterResourceModifierActionWorker.getKubernetesClusterNodeNamePrefix()); + } + + @Test + public void getKubernetesClusterNodeNamePrefixTestNormalizedPrefixShouldContainClusterUuidWhenAllCharactersAreInvalid() { + String clusterUuid = "2699b547-cb56-4a59-a2c6-331cfb21d2e4"; + String originalPrefix = "!@#$%^&*()_+?.|<>"; + String expectedPrefix = "k8s-" + clusterUuid; + + Mockito.when(kubernetesClusterMock.getUuid()).thenReturn(clusterUuid); + Mockito.when(kubernetesClusterMock.getName()).thenReturn(originalPrefix); + Assert.assertEquals(expectedPrefix, kubernetesClusterResourceModifierActionWorker.getKubernetesClusterNodeNamePrefix()); + } + + @Test + public void getKubernetesClusterNodeNamePrefixTestNormalizedPrefixShouldNotStartWithADigit() { + String originalPrefix = "1 cluster"; + String expectedPrefix = "k8s-1cluster"; + + Mockito.when(kubernetesClusterMock.getName()).thenReturn(originalPrefix); + Assert.assertEquals(expectedPrefix, kubernetesClusterResourceModifierActionWorker.getKubernetesClusterNodeNamePrefix()); + } +} diff --git a/utils/src/main/java/com/cloud/utils/net/NetUtils.java b/utils/src/main/java/com/cloud/utils/net/NetUtils.java index 2703deaad649..0df7481f7dca 100644 --- a/utils/src/main/java/com/cloud/utils/net/NetUtils.java +++ b/utils/src/main/java/com/cloud/utils/net/NetUtils.java @@ -1055,13 +1055,23 @@ public static String portRangeToString(final int portRange[]) { return Integer.toString(portRange[0]) + ":" + Integer.toString(portRange[1]); } + /** + * Validates a domain name. + * + *

Domain names must satisfy the following constraints: + *

+ * + * @param hostName The domain name to validate + * @param isHostName If true, verifies whether the domain name starts with a digit + * @return true if the domain name is valid, false otherwise + */ public static boolean verifyDomainNameLabel(final String hostName, final boolean isHostName) { - // must be between 1 and 63 characters long and may contain only the ASCII letters 'a' through 'z' (in a - // case-insensitive manner), - // the digits '0' through '9', and the hyphen ('-'). - // Can not start with a hyphen and digit, and must not end with a hyphen - // If it's a host name, don't allow to start with digit - if (hostName.length() > 63 || hostName.length() < 1) { s_logger.warn("Domain name label must be between 1 and 63 characters long"); return false;