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/>.

16

17 from __future__ import annotations

18

19 from logging import getLogger

20 from urllib.parse import urljoin

21

22 from requests import codes

23 from yaml import safe_load as yaml_safe_load

24

25 from gitlabracadabra.matchers import Matcher

26 from gitlabracadabra.packages.package_file import PackageFile

27 from gitlabracadabra.packages.source import Source

28

29 logger = getLogger(__name__)

30

31

32 class Helm(Source):

33 """Helm repository."""

34

35 def __init__(

36 self,

37 *,

38 log_prefix: str = "",

39 repo_url: str,

40 package_name: str,

41 versions: list[str] | None = None,

42 semver: str | None = None,

43 limit: int | None = 1,

44 channel: str | None = None,

45 ) -> None:

46 """Initialize a Helm repository object.

47

48 Args:

49 log_prefix: Log prefix.

50 repo_url: Helm repository URL.

51 package_name: Package name.

52 versions: List of versions.

53 semver: Semantic version.

54 limit: Keep at most n latest versions.

55 channel: Destination channel.

56 """

57 super().__init__()

58 self._log_prefix = log_prefix

59 self._repo_url = repo_url

60 self._package_name = package_name

61 self._versions = versions or ["/.*/"]

62 self._semver = semver or "*"

63 self._limit = limit

64 self._channel = channel or "stable"

65

66 def __str__(self) -> str:

67 """Return string representation.

68

69 Returns:

70 A string.

71 """

72 return f"Helm charts repository (url={self._repo_url})"

73

74 @property

75 def package_files(self) -> list[PackageFile]:

76 """Return list of package files.

77

78 Returns:

79 List of package files.

80 """

81 package_entries = self._get_helm_index().get("entries", {})

82 package_matches = Matcher(

83 self._package_name,

84 None,

85 log_prefix=self._log_prefix,

86 ).match(

87 list(package_entries.keys()),

88 )

89 package_files: list[PackageFile] = []

90 for package_match in package_matches:

91 package_entry = package_entries[package_match.group(0)]

92 package_versions = {package_dict.get("version", "0"): package_dict for package_dict in package_entry}

93 matches = Matcher(

94 self._versions,

95 self._semver,

96 self._limit,

97 log_prefix=self._log_prefix,

98 ).match(

99 list(package_versions.keys()),

100 )

101 for match in matches:

102 package_files.append(self._package_file(package_versions[match[0]])) # noqa: PERF401

103 if not package_files:

104 logger.warning(

105 "%sPackage not found %s for Helm index %s",

106 self._log_prefix,

107 self._package_name,

108 self._repo_index_url,

109 )

110 return package_files

111

112 def _get_helm_index(self) -> dict:

113 index_response = self.session.request("get", self._repo_index_url)

114 if index_response.status_code != codes["ok"]:

115 logger.warning(

116 "%sUnexpected HTTP status for Helm index %s: received %i %s",

117 self._log_prefix,

118 self._repo_index_url,

119 index_response.status_code,

120 index_response.reason,

121 )

122 return {}

123 return yaml_safe_load(index_response.content) # type: ignore

124

125 @property

126 def _repo_index_url(self) -> str:

127 return f"{self._repo_url}/index.yaml"

128

129 def _package_file(self, package_dict: dict) -> PackageFile:

130 url = urljoin(self._repo_index_url, package_dict.get("urls", []).pop())

131 return PackageFile(

132 url,

133 "helm",

134 package_dict.get("name", self._package_name),

135 package_dict.get("version", "0"),

136 metadata={"channel": self._channel},

137 )