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

47 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-03-10 17:02 +0100

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 urllib.parse import urljoin 

21 

22from requests import codes 

23from yaml import safe_load as yaml_safe_load 

24 

25from gitlabracadabra.matchers import Matcher 

26from gitlabracadabra.packages.package_file import PackageFile 

27from gitlabracadabra.packages.source import Source 

28 

29logger = getLogger(__name__) 

30 

31 

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