Coverage for src/gitlabracadabra/packages/helm.py: 97%
49 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-23 06:44 +0200
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-23 06:44 +0200
1#
2# Copyright (C) 2019-2025 Mathieu Parent <math.parent@gmail.com>
3#
4# This program is free software: you can redistribute it and/or modify
5# it under the terms of the GNU Lesser General Public License as published by
6# the Free Software Foundation, either version 3 of the License, or
7# (at your option) any later version.
8#
9# This program is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with this program. If not, see <http://www.gnu.org/licenses/>.
17from __future__ import annotations
19from logging import getLogger
20from typing import TYPE_CHECKING
21from urllib.parse import urljoin
23from requests import codes
24from yaml import safe_load as yaml_safe_load
26from gitlabracadabra.matchers import Matcher
27from gitlabracadabra.packages.package_file import PackageFile
28from gitlabracadabra.packages.source import Source
30if TYPE_CHECKING: 30 ↛ 31line 30 didn't jump to line 31 because the condition on line 30 was never true
31 from gitlabracadabra.packages.destination import Destination
33logger = getLogger(__name__)
36class Helm(Source):
37 """Helm repository."""
39 def __init__(
40 self,
41 *,
42 log_prefix: str = "",
43 repo_url: str,
44 package_name: str,
45 versions: list[str] | None = None,
46 semver: str | None = None,
47 limit: int | None = 1,
48 channel: str | None = None,
49 ) -> None:
50 """Initialize a Helm repository object.
52 Args:
53 log_prefix: Log prefix.
54 repo_url: Helm repository URL.
55 package_name: Package name.
56 versions: List of versions.
57 semver: Semantic version.
58 limit: Keep at most n latest versions.
59 channel: Destination channel.
60 """
61 super().__init__()
62 self._log_prefix = log_prefix
63 self._repo_url = repo_url
64 self._package_name = package_name
65 self._versions = versions or ["/.*/"]
66 self._semver = semver or "*"
67 self._limit = limit
68 self._channel = channel or "stable"
70 def __str__(self) -> str:
71 """Return string representation.
73 Returns:
74 A string.
75 """
76 return f"Helm charts repository (url={self._repo_url})"
78 def package_files(
79 self,
80 destination: Destination, # noqa: ARG002
81 ) -> list[PackageFile]:
82 """Return list of package files.
84 Returns:
85 List of package files.
86 """
87 package_entries = self._get_helm_index().get("entries", {})
88 package_matches = Matcher(
89 self._package_name,
90 None,
91 log_prefix=self._log_prefix,
92 ).match(
93 list(package_entries.keys()),
94 )
95 package_files: list[PackageFile] = []
96 for package_match in package_matches:
97 package_entry = package_entries[package_match.group(0)]
98 package_versions = {package_dict.get("version", "0"): package_dict for package_dict in package_entry}
99 matches = Matcher(
100 self._versions,
101 self._semver,
102 self._limit,
103 log_prefix=self._log_prefix,
104 ).match(
105 list(package_versions.keys()),
106 )
107 for match in matches:
108 package_files.append(self._package_file(package_versions[match[0]])) # noqa: PERF401
109 if not package_files:
110 logger.warning(
111 "%sPackage not found %s for Helm index %s",
112 self._log_prefix,
113 self._package_name,
114 self._repo_index_url,
115 )
116 return package_files
118 def _get_helm_index(self) -> dict:
119 index_response = self.session.request("get", self._repo_index_url)
120 if index_response.status_code != codes["ok"]:
121 logger.warning(
122 "%sUnexpected HTTP status for Helm index %s: received %i %s",
123 self._log_prefix,
124 self._repo_index_url,
125 index_response.status_code,
126 index_response.reason,
127 )
128 return {}
129 return yaml_safe_load(index_response.content) # type: ignore
131 @property
132 def _repo_index_url(self) -> str:
133 return f"{self._repo_url}/index.yaml"
135 def _package_file(self, package_dict: dict) -> PackageFile:
136 url = urljoin(self._repo_index_url, package_dict.get("urls", []).pop())
137 return PackageFile(
138 url,
139 "helm",
140 package_dict.get("name", self._package_name),
141 package_dict.get("version", "0"),
142 metadata={"channel": self._channel},
143 )