From 1c1a25631024278b7caeb6a1dde34d5de326df6c Mon Sep 17 00:00:00 2001 From: Gabriel Cuba Date: Mon, 20 Nov 2023 01:08:39 -0500 Subject: [PATCH] Feature 10997: Adds helm OCI registry login Change-Id: I1bc12bdf52f082900c3388d03c31e52841017b94 Signed-off-by: Gabriel Cuba --- n2vc/k8s_helm3_conn.py | 4 +- n2vc/k8s_helm_base_conn.py | 111 ++++++++++++++++--------- n2vc/tests/unit/test_k8s_helm3_conn.py | 14 +++- 3 files changed, 85 insertions(+), 44 deletions(-) diff --git a/n2vc/k8s_helm3_conn.py b/n2vc/k8s_helm3_conn.py index 675c851..14f7fe0 100644 --- a/n2vc/k8s_helm3_conn.py +++ b/n2vc/k8s_helm3_conn.py @@ -358,8 +358,8 @@ class K8sHelm3Connector(K8sHelmBaseConnector): Args: show_command: the second part of the command (`helm show `) - 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: diff --git a/n2vc/k8s_helm_base_conn.py b/n2vc/k8s_helm_base_conn.py index 383ce7d..5f004b3 100644 --- a/n2vc/k8s_helm_base_conn.py +++ b/n2vc/k8s_helm_base_conn.py @@ -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 ): diff --git a/n2vc/tests/unit/test_k8s_helm3_conn.py b/n2vc/tests/unit/test_k8s_helm3_conn.py index a2e75e1..bddfddd 100644 --- a/n2vc/tests/unit/test_k8s_helm3_conn.py +++ b/n2vc/tests/unit/test_k8s_helm3_conn.py @@ -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( -- 2.17.1