Feature 10997: Adds helm OCI registry login
Change-Id: I1bc12bdf52f082900c3388d03c31e52841017b94
Signed-off-by: Gabriel Cuba <gcuba@whitestack.com>
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 @@
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:
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 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 @@
cert: str = None,
user: str = None,
password: str = None,
+ oci: bool = False,
):
self.log.debug(
"Cluster {}, adding {} repository {}. URL: {}".format(
@@ -180,10 +182,23 @@
# 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 @@
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 @@
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 @@
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 @@
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 @@
)
# 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 @@
# 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 @@
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 @@
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 @@
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 @@
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 @@
}
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 @@
}
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 @@
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 @@
"--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 @@
)
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(