diff --git a/.github/workflows/ui.yml b/.github/workflows/ui.yml index 56f757133b7..88d4a70e4c2 100644 --- a/.github/workflows/ui.yml +++ b/.github/workflows/ui.yml @@ -56,6 +56,7 @@ jobs: npm run test:unit - uses: codecov/codecov-action@v4 + if: github.repository == 'apache/cloudstack' with: working-directory: ui files: ./coverage/lcov.info diff --git a/core/src/main/java/org/apache/cloudstack/direct/download/HttpsDirectTemplateDownloader.java b/core/src/main/java/org/apache/cloudstack/direct/download/HttpsDirectTemplateDownloader.java index b8a25a11b5c..338eb773257 100644 --- a/core/src/main/java/org/apache/cloudstack/direct/download/HttpsDirectTemplateDownloader.java +++ b/core/src/main/java/org/apache/cloudstack/direct/download/HttpsDirectTemplateDownloader.java @@ -39,9 +39,7 @@ import java.util.Map; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLContext; -import javax.net.ssl.TrustManager; -import org.apache.cloudstack.utils.security.SSLUtils; import org.apache.commons.collections.MapUtils; import org.apache.commons.httpclient.HttpStatus; import org.apache.commons.io.IOUtils; @@ -55,6 +53,7 @@ import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.conn.ssl.SSLConnectionSocketFactory; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; +import org.apache.http.ssl.SSLContexts; import org.apache.http.util.EntityUtils; import com.cloud.utils.Pair; @@ -120,10 +119,10 @@ public class HttpsDirectTemplateDownloader extends DirectTemplateDownloaderImpl String password = "changeit"; defaultKeystore.load(is, password.toCharArray()); } - TrustManager[] tm = HttpsMultiTrustManager.getTrustManagersFromKeyStores(customKeystore, defaultKeystore); - SSLContext sslContext = SSLUtils.getSSLContext(); - sslContext.init(null, tm, null); - return sslContext; + return SSLContexts.custom() + .loadTrustMaterial(customKeystore, null) + .loadTrustMaterial(defaultKeystore, null) + .build(); } catch (KeyStoreException | NoSuchAlgorithmException | CertificateException | IOException | KeyManagementException e) { logger.error(String.format("Failure getting SSL context for HTTPS downloader, using default SSL context: %s", e.getMessage()), e); try { diff --git a/core/src/main/java/org/apache/cloudstack/direct/download/HttpsMultiTrustManager.java b/core/src/main/java/org/apache/cloudstack/direct/download/HttpsMultiTrustManager.java deleted file mode 100644 index fe47847c36c..00000000000 --- a/core/src/main/java/org/apache/cloudstack/direct/download/HttpsMultiTrustManager.java +++ /dev/null @@ -1,102 +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 org.apache.cloudstack.direct.download; - -import java.security.KeyStore; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.security.cert.CertificateException; -import java.security.cert.X509Certificate; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -import javax.net.ssl.TrustManager; -import javax.net.ssl.TrustManagerFactory; -import javax.net.ssl.X509TrustManager; - -import com.google.common.collect.ImmutableList; -import com.google.common.collect.Iterables; - -public class HttpsMultiTrustManager implements X509TrustManager { - - private final List trustManagers; - - public HttpsMultiTrustManager(KeyStore... keystores) { - List trustManagers = new ArrayList<>(); - trustManagers.add(getTrustManager(null)); - for (KeyStore keystore : keystores) { - trustManagers.add(getTrustManager(keystore)); - } - this.trustManagers = ImmutableList.copyOf(trustManagers); - } - - public static TrustManager[] getTrustManagersFromKeyStores(KeyStore... keyStore) { - return new TrustManager[] { new HttpsMultiTrustManager(keyStore) }; - - } - - @Override - public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { - for (X509TrustManager trustManager : trustManagers) { - try { - trustManager.checkClientTrusted(chain, authType); - return; - } catch (CertificateException ignored) {} - } - throw new CertificateException("None of the TrustManagers trust this certificate chain"); - } - - @Override - public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { - for (X509TrustManager trustManager : trustManagers) { - try { - trustManager.checkServerTrusted(chain, authType); - return; - } catch (CertificateException ignored) {} - } - throw new CertificateException("None of the TrustManagers trust this certificate chain"); - } - - @Override - public X509Certificate[] getAcceptedIssuers() { - ImmutableList.Builder certificates = ImmutableList.builder(); - for (X509TrustManager trustManager : trustManagers) { - for (X509Certificate cert : trustManager.getAcceptedIssuers()) { - certificates.add(cert); - } - } - return Iterables.toArray(certificates.build(), X509Certificate.class); - } - - public X509TrustManager getTrustManager(KeyStore keystore) { - return getTrustManager(TrustManagerFactory.getDefaultAlgorithm(), keystore); - } - - public X509TrustManager getTrustManager(String algorithm, KeyStore keystore) { - TrustManagerFactory factory; - try { - factory = TrustManagerFactory.getInstance(algorithm); - factory.init(keystore); - return Iterables.getFirst(Iterables.filter( - Arrays.asList(factory.getTrustManagers()), X509TrustManager.class), null); - } catch (NoSuchAlgorithmException | KeyStoreException e) { - e.printStackTrace(); - } - return null; - } -} diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtSetupDirectDownloadCertificateCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtSetupDirectDownloadCertificateCommandWrapper.java index 62cca7eb209..e320baad717 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtSetupDirectDownloadCertificateCommandWrapper.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtSetupDirectDownloadCertificateCommandWrapper.java @@ -84,7 +84,7 @@ public class LibvirtSetupDirectDownloadCertificateCommandWrapper extends Command private void importCertificate(String tempCerFilePath, String keyStoreFile, String certificateName, String privatePassword) { logger.debug("Importing certificate from temporary file to keystore"); String keyToolPath = Script.getExecutableAbsolutePath("keytool"); - int result = Script.executeCommandForExitValue(keyToolPath, "-importcert", "file", tempCerFilePath, + int result = Script.executeCommandForExitValue(keyToolPath, "-importcert", "-file", tempCerFilePath, "-keystore", keyStoreFile, "-alias", sanitizeBashCommandArgument(certificateName), "-storepass", privatePassword, "-noprompt"); if (result != 0) { 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 8c983149d02..5dfab87dd71 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 @@ -159,6 +159,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); } @@ -775,19 +777,35 @@ public class KubernetesClusterResourceModifierActionWorker extends KubernetesClu } } + /** + * Generates a valid name prefix for Kubernetes cluster nodes. + * + *

The prefix must comply with Kubernetes naming constraints: + *

    + *
  • Maximum 63 characters total
  • + *
  • Only lowercase alphanumeric characters and hyphens
  • + *
  • Must start with a letter
  • + *
  • Must end with an alphanumeric character
  • + *
+ * + *

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 00000000000..c220a3468af --- /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/server/src/main/java/com/cloud/template/TemplateAdapterBase.java b/server/src/main/java/com/cloud/template/TemplateAdapterBase.java index 5b7185b94c5..3f2c07ca8a5 100644 --- a/server/src/main/java/com/cloud/template/TemplateAdapterBase.java +++ b/server/src/main/java/com/cloud/template/TemplateAdapterBase.java @@ -194,7 +194,8 @@ public abstract class TemplateAdapterBase extends AdapterBase implements Templat if (!isAdmin && zoneIdList == null && !isRegionStore ) { // domain admin and user should also be able to register template on a region store - throw new InvalidParameterValueException("Please specify a valid zone Id. Only admins can create templates in all zones."); + throw new InvalidParameterValueException("Template registered for 'All zones' can only be owned a Root Admin account. " + + "Please select specific zone(s)."); } // check for the url format only when url is not null. url can be null incase of form based upload 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 0e5834c4de9..d41025bd60e 100644 --- a/utils/src/main/java/com/cloud/utils/net/NetUtils.java +++ b/utils/src/main/java/com/cloud/utils/net/NetUtils.java @@ -1058,13 +1058,23 @@ public class NetUtils { return Integer.toString(portRange[0]) + ":" + Integer.toString(portRange[1]); } + /** + * Validates a domain name. + * + *

Domain names must satisfy the following constraints: + *

    + *
  • Length between 1 and 63 characters
  • + *
  • Contain only ASCII letters 'a' through 'z' (case-insensitive)
  • + *
  • Can include digits '0' through '9' and hyphens (-)
  • + *
  • Must not start or end with a hyphen
  • + *
  • If used as hostname, must not start with a digit
  • + *
+ * + * @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) { LOGGER.warn("Domain name label must be between 1 and 63 characters long"); return false; diff --git a/vmware-base/src/main/java/com/cloud/hypervisor/vmware/mo/BaseMO.java b/vmware-base/src/main/java/com/cloud/hypervisor/vmware/mo/BaseMO.java index 2ced55e1ac2..ead1d8a7213 100644 --- a/vmware-base/src/main/java/com/cloud/hypervisor/vmware/mo/BaseMO.java +++ b/vmware-base/src/main/java/com/cloud/hypervisor/vmware/mo/BaseMO.java @@ -253,12 +253,29 @@ public class BaseMO { hostClusterPair = hostClusterNamesMap.get(hostMorValue); } else { HostMO hostMO = new HostMO(_context, hostMor); - ClusterMO clusterMO = new ClusterMO(_context, hostMO.getHyperHostCluster()); - hostClusterPair = new Pair<>(hostMO.getHostName(), clusterMO.getName()); + String hostName = hostMO.getHostName(); + String clusterName = getClusterNameFromHostIncludingStandaloneHosts(hostMO, hostName); + hostClusterPair = new Pair<>(hostName, clusterName); hostClusterNamesMap.put(hostMorValue, hostClusterPair); } vm.setHostName(hostClusterPair.first()); vm.setClusterName(hostClusterPair.second()); } } + + /** + * Return the cluster name of the host on the vCenter + * @return null in case the host is standalone (doesn't belong to a cluster), cluster name otherwise + */ + private String getClusterNameFromHostIncludingStandaloneHosts(HostMO hostMO, String hostName) { + try { + ClusterMO clusterMO = new ClusterMO(_context, hostMO.getHyperHostCluster()); + return clusterMO.getName(); + } catch (Exception e) { + String msg = String.format("Cannot find a cluster for host %s, assuming standalone host, " + + "setting its cluster name as empty", hostName); + logger.info(msg); + return null; + } + } }