Feature 10997: Adds helm OCI registry login 43/14043/4 release-v15.0-start
authorGabriel Cuba <gcuba@whitestack.com>
Mon, 20 Nov 2023 06:08:39 +0000 (01:08 -0500)
committercubag <gcuba@whitestack.com>
Fri, 1 Dec 2023 06:54:40 +0000 (07:54 +0100)
Change-Id: I1bc12bdf52f082900c3388d03c31e52841017b94
Signed-off-by: Gabriel Cuba <gcuba@whitestack.com>
n2vc/k8s_helm3_conn.py
n2vc/k8s_helm_base_conn.py
n2vc/tests/unit/test_k8s_helm3_conn.py

index 675c851..14f7fe0 100644 (file)
@@ -358,8 +358,8 @@ class K8sHelm3Connector(K8sHelmBaseConnector):
 
         Args:
             show_command: the second part of the command (`helm show <show_command>`)
-            kdu_model: The name or path of an Helm Chart
-            repo_url: Helm Chart repository url
+            kdu_model: The name or path of a Helm Chart
+            repo_str: Helm Chart repository url
             version: constraint with specific version of the Chart to use
 
         Returns:
index 383ce7d..5f004b3 100644 (file)
@@ -31,6 +31,7 @@ import stat
 import os
 import yaml
 from uuid import uuid4
+from urllib.parse import urlparse
 
 from n2vc.config import EnvironConfig
 from n2vc.exceptions import K8sException
@@ -165,6 +166,7 @@ class K8sHelmBaseConnector(K8sConnector):
         cert: str = None,
         user: str = None,
         password: str = None,
+        oci: bool = False,
     ):
         self.log.debug(
             "Cluster {}, adding {} repository {}. URL: {}".format(
@@ -180,10 +182,23 @@ class K8sHelmBaseConnector(K8sConnector):
         # sync local dir
         self.fs.sync(from_path=cluster_uuid)
 
-        # helm repo add name url
-        command = ("env KUBECONFIG={} {} repo add {} {}").format(
-            paths["kube_config"], self._helm_command, quote(name), quote(url)
-        )
+        if oci:
+            if user and password:
+                host_port = urlparse(url).netloc if url.startswith("oci://") else url
+                # helm registry login url
+                command = "env KUBECONFIG={} {} registry login {}".format(
+                    paths["kube_config"], self._helm_command, quote(host_port)
+                )
+            else:
+                self.log.debug(
+                    "OCI registry login is not needed for repo: {}".format(name)
+                )
+                return
+        else:
+            # helm repo add name url
+            command = "env KUBECONFIG={} {} repo add {} {}".format(
+                paths["kube_config"], self._helm_command, quote(name), quote(url)
+            )
 
         if cert:
             temp_cert_file = os.path.join(
@@ -205,14 +220,15 @@ class K8sHelmBaseConnector(K8sConnector):
             command=command, raise_exception_on_error=True, env=env
         )
 
-        # helm repo update
-        command = "env KUBECONFIG={} {} repo update {}".format(
-            paths["kube_config"], self._helm_command, quote(name)
-        )
-        self.log.debug("updating repo: {}".format(command))
-        await self._local_async_exec(
-            command=command, raise_exception_on_error=False, env=env
-        )
+        if not oci:
+            # helm repo update
+            command = "env KUBECONFIG={} {} repo update {}".format(
+                paths["kube_config"], self._helm_command, quote(name)
+            )
+            self.log.debug("updating repo: {}".format(command))
+            await self._local_async_exec(
+                command=command, raise_exception_on_error=False, env=env
+            )
 
         # sync fs
         self.fs.reverse_sync(from_path=cluster_uuid)
@@ -379,6 +395,11 @@ class K8sHelmBaseConnector(K8sConnector):
     def _is_helm_chart_a_file(self, chart_name: str):
         return chart_name.count("/") > 1
 
+    @staticmethod
+    def _is_helm_chart_a_url(chart_name: str):
+        result = urlparse(chart_name)
+        return all([result.scheme, result.netloc])
+
     async def _install_impl(
         self,
         cluster_id: str,
@@ -403,12 +424,7 @@ class K8sHelmBaseConnector(K8sConnector):
             cluster_id=cluster_id, params=params
         )
 
-        # version
-        kdu_model, version = self._split_version(kdu_model)
-
-        _, repo = self._split_repo(kdu_model)
-        if repo:
-            await self.repo_update(cluster_id, repo)
+        kdu_model, version = await self._prepare_helm_chart(kdu_model, cluster_id)
 
         command = self._get_install_command(
             kdu_model,
@@ -512,12 +528,7 @@ class K8sHelmBaseConnector(K8sConnector):
             cluster_id=cluster_uuid, params=params
         )
 
-        # version
-        kdu_model, version = self._split_version(kdu_model)
-
-        _, repo = self._split_repo(kdu_model)
-        if repo:
-            await self.repo_update(cluster_uuid, repo)
+        kdu_model, version = await self._prepare_helm_chart(kdu_model, cluster_uuid)
 
         command = self._get_upgrade_command(
             kdu_model,
@@ -645,7 +656,7 @@ class K8sHelmBaseConnector(K8sConnector):
         )
 
         # version
-        kdu_model, version = self._split_version(kdu_model)
+        kdu_model, version = await self._prepare_helm_chart(kdu_model, cluster_uuid)
 
         repo_url = await self._find_repo(kdu_model, cluster_uuid)
 
@@ -1198,19 +1209,15 @@ class K8sHelmBaseConnector(K8sConnector):
 
                         # add repo
                         self.log.debug("add repo {}".format(db_repo["name"]))
-                        if "ca_cert" in db_repo:
-                            await self.repo_add(
-                                cluster_uuid,
-                                db_repo["name"],
-                                db_repo["url"],
-                                cert=db_repo["ca_cert"],
-                            )
-                        else:
-                            await self.repo_add(
-                                cluster_uuid,
-                                db_repo["name"],
-                                db_repo["url"],
-                            )
+                        await self.repo_add(
+                            cluster_uuid,
+                            db_repo["name"],
+                            db_repo["url"],
+                            cert=db_repo.get("ca_cert"),
+                            user=db_repo.get("user"),
+                            password=db_repo.get("password"),
+                            oci=db_repo.get("oci", False),
+                        )
                         added_repo_dict[repo_id] = db_repo["name"]
                 except Exception as e:
                     raise K8sException(
@@ -2037,7 +2044,13 @@ class K8sHelmBaseConnector(K8sConnector):
 
     def _split_version(self, kdu_model: str) -> tuple[str, str]:
         version = None
-        if not self._is_helm_chart_a_file(kdu_model) and ":" in kdu_model:
+        if (
+            not (
+                self._is_helm_chart_a_file(kdu_model)
+                or self._is_helm_chart_a_url(kdu_model)
+            )
+            and ":" in kdu_model
+        ):
             parts = kdu_model.split(sep=":")
             if len(parts) == 2:
                 version = str(parts[1])
@@ -2060,7 +2073,7 @@ class K8sHelmBaseConnector(K8sConnector):
         repo_name = None
 
         idx = kdu_model.find("/")
-        if idx >= 0:
+        if not self._is_helm_chart_a_url(kdu_model) and idx >= 0:
             chart_name = kdu_model[idx + 1 :]
             repo_name = kdu_model[:idx]
 
@@ -2090,6 +2103,24 @@ class K8sHelmBaseConnector(K8sConnector):
 
         return repo_url
 
+    def _repo_to_oci_url(self, repo):
+        db_repo = self.db.get_one("k8srepos", {"name": repo}, fail_on_empty=False)
+        if db_repo and "oci" in db_repo:
+            return db_repo.get("url")
+
+    async def _prepare_helm_chart(self, kdu_model, cluster_id):
+        # e.g.: "stable/openldap", "1.0"
+        kdu_model, version = self._split_version(kdu_model)
+        # e.g.: "openldap, stable"
+        chart_name, repo = self._split_repo(kdu_model)
+        if repo and chart_name:  # repo/chart case
+            oci_url = self._repo_to_oci_url(repo)
+            if oci_url:  # oci does not require helm repo update
+                kdu_model = f"{oci_url.rstrip('/')}/{chart_name.lstrip('/')}"  # urljoin doesn't work for oci schema
+            else:
+                await self.repo_update(cluster_id, repo)
+        return kdu_model, version
+
     async def create_certificate(
         self, cluster_uuid, namespace, dns_prefix, name, secret_name, usage
     ):
index a2e75e1..bddfddd 100644 (file)
@@ -172,6 +172,7 @@ class TestK8sHelm3Conn(asynctest.TestCase):
         self.helm_conn._local_async_exec = asynctest.CoroutineMock(return_value=("", 0))
         self.helm_conn._status_kdu = asynctest.CoroutineMock(return_value=None)
         self.helm_conn._store_status = asynctest.CoroutineMock()
+        self.helm_conn._repo_to_oci_url = Mock(return_value=None)
         self.kdu_instance = "stable-openldap-0005399828"
         self.helm_conn.generate_kdu_instance_name = Mock(return_value=self.kdu_instance)
         self.helm_conn._get_namespaces = asynctest.CoroutineMock(return_value=[])
@@ -266,6 +267,7 @@ class TestK8sHelm3Conn(asynctest.TestCase):
         }
         self.helm_conn._local_async_exec = asynctest.CoroutineMock(return_value=("", 0))
         self.helm_conn._store_status = asynctest.CoroutineMock()
+        self.helm_conn._repo_to_oci_url = Mock(return_value=None)
         self.helm_conn.get_instance_info = asynctest.CoroutineMock(
             return_value=instance_info
         )
@@ -348,6 +350,7 @@ class TestK8sHelm3Conn(asynctest.TestCase):
         }
         self.helm_conn._local_async_exec = asynctest.CoroutineMock(return_value=("", 0))
         self.helm_conn._store_status = asynctest.CoroutineMock()
+        self.helm_conn._repo_to_oci_url = Mock(return_value=None)
         self.helm_conn.get_instance_info = asynctest.CoroutineMock(
             return_value=instance_info
         )
@@ -416,6 +419,7 @@ class TestK8sHelm3Conn(asynctest.TestCase):
         self.helm_conn.values_kdu = asynctest.CoroutineMock(return_value=kdu_values)
         self.helm_conn._local_async_exec = asynctest.CoroutineMock(return_value=("", 0))
         self.helm_conn._store_status = asynctest.CoroutineMock()
+        self.helm_conn._repo_to_oci_url = Mock(return_value=None)
         self.helm_conn.get_instance_info = asynctest.CoroutineMock(
             return_value=instance_info
         )
@@ -436,7 +440,7 @@ class TestK8sHelm3Conn(asynctest.TestCase):
             "--namespace testk8s --atomic --output yaml --set replicaCount=2 --timeout 1800s "
             "--reuse-values --version 1.2.3"
         )
-        self.helm_conn._local_async_exec.assert_called_once_with(
+        self.helm_conn._local_async_exec.assert_called_with(
             command=command, env=self.env, raise_exception_on_error=False
         )
         # TEST-2
@@ -798,7 +802,13 @@ class TestK8sHelm3Conn(asynctest.TestCase):
         )
         self.helm_conn.repo_remove.assert_not_called()
         self.helm_conn.repo_add.assert_called_once_with(
-            self.cluster_uuid, "bitnami", "https://charts.bitnami.com/bitnami"
+            self.cluster_uuid,
+            "bitnami",
+            "https://charts.bitnami.com/bitnami",
+            cert=None,
+            user=None,
+            password=None,
+            oci=False,
         )
         self.assertEqual(deleted_repo_list, [], "Deleted repo list should be empty")
         self.assertEqual(