Coverage for src/gitlabracadabra/packages/helm.py: 97%

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 typing import TYPE_CHECKING

21 from urllib.parse import urljoin

22

23 from requests import codes

24 from yaml import safe_load as yaml_safe_load

25

26 from gitlabracadabra.matchers import Matcher

27 from gitlabracadabra.packages.package_file import PackageFile

28 from gitlabracadabra.packages.source import Source

29

30 if 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

32

33 logger = getLogger(__name__)

34

35

36 class Helm(Source):

37 """Helm repository."""

38

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.

51

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"

69

70 def __str__(self) -> str:

71 """Return string representation.

72

73 Returns:

74 A string.

75 """

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

77

78 def package_files(

79 self,

80 destination: Destination, # noqa: ARG002

81 ) -> list[PackageFile]:

82 """Return list of package files.

83

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

117

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

130

131 @property

132 def _repo_index_url(self) -> str:

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

134

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 )