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

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 

17from __future__ import annotations 

18 

19from logging import getLogger 

20from typing import TYPE_CHECKING 

21from urllib.parse import urljoin 

22 

23from requests import codes 

24from yaml import safe_load as yaml_safe_load 

25 

26from gitlabracadabra.matchers import Matcher 

27from gitlabracadabra.packages.package_file import PackageFile 

28from gitlabracadabra.packages.source import Source 

29 

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 

32 

33logger = getLogger(__name__) 

34 

35 

36class 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 )