Coverage for src/gitlabracadabra/mixins/image_mirrors.py: 82%

91 statements  

« prev     ^ index     » next       coverage.py v7.8.0, created at 2025-04-14 23:10 +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 

19import logging 

20from pathlib import PurePosixPath 

21from re import Match 

22from typing import TYPE_CHECKING 

23 

24from gitlabracadabra.containers.registries import ReferenceParts, Registries 

25from gitlabracadabra.matchers import Matcher 

26from gitlabracadabra.objects.object import GitLabracadabraObject 

27 

28if TYPE_CHECKING: 28 ↛ 29line 28 didn't jump to line 29 because the condition on line 28 was never true

29 from typing import Any 

30 

31 from gitlabracadabra.containers.registry import Registry 

32 

33 

34logger = logging.getLogger(__name__) 

35 

36Source = tuple[PurePosixPath, str, Match, str | None] 

37 

38 

39class ImageMirrorsMixin(GitLabracadabraObject): 

40 """Object (Project) with image mirrors.""" 

41 

42 def _process_image_mirrors( 

43 self, 

44 param_name: str, 

45 param_value: Any, 

46 *, 

47 dry_run: bool = False, 

48 skip_save: bool = False, 

49 ) -> None: 

50 """Process the image_mirrors param. 

51 

52 Args: 

53 param_name: "image_mirrors". 

54 param_value: List of image mirror dicts. 

55 dry_run: Dry run. 

56 skip_save: False. 

57 """ 

58 assert param_name == "image_mirrors" # noqa: S101 

59 assert not skip_save # noqa: S101 

60 

61 dest_registry, prefix = self._get_destination() 

62 

63 for image_mirror in param_value: 

64 if not image_mirror.get("enabled", True): 64 ↛ 65line 64 didn't jump to line 65 because the condition on line 64 was never true

65 continue 

66 self._mirror(image_mirror, dest_registry, prefix, dry_run=dry_run) 

67 

68 def _get_destination(self) -> tuple[Registry, PurePosixPath]: 

69 # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/54090 

70 try: 

71 container_registry_image_prefix = self._obj.container_registry_image_prefix 

72 except AttributeError: 

73 container_registry_image_prefix = None 

74 if not isinstance(container_registry_image_prefix, str): 74 ↛ 75line 74 didn't jump to line 75 because the condition on line 74 was never true

75 msg = f"Unexpected type for container_registry_image_prefix: {type(container_registry_image_prefix)}" 

76 raise TypeError(msg) 

77 

78 netloc, path = container_registry_image_prefix.split("/", 1) 

79 registry = Registries().get_registry(netloc, self.connection.registry_session_callback) 

80 return (registry, PurePosixPath(path)) 

81 

82 def _mirror( 

83 self, 

84 image_mirror: dict, 

85 dest_registry: Registry, 

86 prefix: PurePosixPath, 

87 *, 

88 dry_run: bool, 

89 ) -> None: 

90 sources = self._get_sources(image_mirror.get("from"), image_mirror.get("semver")) 

91 for source in sources: 

92 dest = self._get_dest( 

93 source[0], 

94 source[1], 

95 source[2], 

96 image_mirror.get("to"), 

97 ) 

98 digest_suffix = "" 

99 if source[3]: 

100 digest_suffix = f"@{source[3]}" 

101 source_manifest = Registries().get_manifest(f"{source[0] / source[1]!s}:{source[2][0]}{digest_suffix}") 

102 source_manifest.forced_digest = source[3] is not None 

103 dest_manifest = Registries().get_manifest( 

104 ReferenceParts( 

105 dest_registry.hostname, 

106 str(prefix / dest[0]), 

107 dest[1], 

108 None, 

109 ) 

110 ) 

111 dest_manifest.registry.import_manifest( 

112 source_manifest, 

113 dest_manifest.manifest_name, 

114 tag=dest_manifest.tag, 

115 platform=None, 

116 log_prefix=f"[{self._name}] ", 

117 dry_run=dry_run, 

118 ) 

119 

120 def _get_sources(self, from_param: Any, semver: str | None) -> list[Source]: 

121 """Get sources. 

122 

123 Args: 

124 from_param: The "from" param. 

125 semver: Optional "semver" param. 

126 

127 Returns: 

128 A list of tuples (base, repository, tag, digest) 

129 

130 Raises: 

131 ValueError: Unexpected from param type. 

132 """ 

133 default_tag = "latest" 

134 if semver: 

135 default_tag = "/.*/" 

136 if isinstance(from_param, str): 

137 parts = Registries().full_reference_parts(from_param) 

138 return self._get_sources_from_parts( 

139 PurePosixPath(parts.hostname), 

140 [parts.manifest_name], 

141 [parts.tag or default_tag], 

142 parts.digest, 

143 semver, 

144 ) 

145 if isinstance(from_param, dict): 145 ↛ 150line 145 didn't jump to line 150 because the condition on line 145 was always true

146 base = PurePosixPath(from_param.get("base", "")) 

147 repositories = from_param.get("repositories", None) 

148 tags = from_param.get("tags", [default_tag]) 

149 return self._get_sources_from_parts(base, repositories, tags, None, semver) 

150 msg = f"Unexpected from param type: {type(from_param)}" 

151 raise ValueError(msg) 

152 

153 def _get_sources_from_parts( 

154 self, 

155 base: PurePosixPath, 

156 repositories: list[str], 

157 tags: list[str], 

158 digest: str | None, 

159 semver: str | None, 

160 ) -> list[Source]: 

161 if not isinstance(repositories, list): 161 ↛ 162line 161 didn't jump to line 162 because the condition on line 161 was never true

162 msg = f"Unexpected from.repositories param type: {type(repositories)}" # type: ignore[unreachable] 

163 raise TypeError(msg) 

164 if not isinstance(tags, list): 164 ↛ 165line 164 didn't jump to line 165 because the condition on line 164 was never true

165 msg = f"Unexpected from.tags param type: {type(tags)}" # type: ignore[unreachable] 

166 raise TypeError(msg) 

167 sources: list[Source] = [] 

168 for repository in repositories: 

169 matcher = Matcher( 

170 tags, 

171 semver, 

172 log_prefix=f"[{self._name}] ", 

173 ) 

174 manifest = Registries().get_manifest(str(base / repository)) 

175 for match in matcher.match(manifest.tag_list): 

176 sources.append((base, repository, match, digest)) # noqa: PERF401 

177 return sources 

178 

179 def _get_dest( 

180 self, 

181 source_base: PurePosixPath, 

182 source_repository: str, 

183 source_tag: Match, 

184 to_param: Any, 

185 ) -> tuple[str, str]: 

186 """Get Destination. 

187 

188 Args: 

189 source_base: base as PurePosixPath. 

190 source_repository: source repository, relative to base. 

191 source_tag: source tag. 

192 source_digest: source digest. 

193 to_param: "to" parameter. 

194 

195 Returns: 

196 A tuple (repository, tag) 

197 

198 Raises: 

199 ValueError: Unexpected to param type. 

200 """ 

201 if to_param is None: 

202 repository = str(source_base / source_repository).split("/", 1).pop() 

203 return repository, source_tag[0] 

204 if isinstance(to_param, str): 

205 tag: str | None 

206 try: 

207 to_param, tag = to_param.rsplit(":", 1) 

208 except ValueError: 

209 tag = None 

210 return to_param, tag or source_tag[0] 

211 if isinstance(to_param, dict): 211 ↛ 213line 211 didn't jump to line 213 because the condition on line 211 was always true

212 return self._get_dest_from_dict(source_base, source_repository, source_tag, to_param) 

213 msg = f"Unexpected to param type: {type(to_param)}" 

214 raise ValueError(msg) 

215 

216 def _get_dest_from_dict( 

217 self, 

218 source_base: PurePosixPath, 

219 source_repository: str, 

220 source_tag: Match, 

221 to_param: dict, 

222 ) -> tuple[str, str]: 

223 base = PurePosixPath(to_param.get("base", source_base) or "") 

224 repository = to_param.get("repository", source_repository) or "" 

225 tag = source_tag.expand(to_param.get("tag") or source_tag[0]) 

226 return str(base / repository), tag