Coverage for src/gitlabracadabra/containers/registries.py: 97%

70 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 

19from re import search as re_search 

20from re import sub as re_sub 

21from typing import TYPE_CHECKING, NamedTuple 

22 

23from gitlabracadabra.containers.const import DOCKER_HOSTNAME, DOCKER_REGISTRY 

24from gitlabracadabra.containers.manifest import Manifest 

25from gitlabracadabra.containers.registry import Registry 

26from gitlabracadabra.singleton import SingletonMeta 

27 

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

29 from collections.abc import Callable 

30 

31 from gitlabracadabra.containers.authenticated_session import AuthenticatedSession 

32 

33 

34class ReferenceParts(NamedTuple): 

35 hostname: str 

36 manifest_name: str 

37 tag: str | None 

38 digest: str | None 

39 

40 

41class Registries(metaclass=SingletonMeta): 

42 """All registies by name.""" 

43 

44 def __init__(self) -> None: 

45 """All connected registries. 

46 

47 Intented to be used as a singleton. 

48 """ 

49 self._registries: dict[str, Registry] = {} 

50 

51 def reset(self) -> None: 

52 """Reset registry cache.""" 

53 self._registries = {} 

54 

55 def get_registry( 

56 self, 

57 hostname: str, 

58 session_callback: Callable[[AuthenticatedSession], None] | None = None, 

59 ) -> Registry: 

60 """Get a registry connection. 

61 

62 Args: 

63 hostname: fqdn of a registry. 

64 session_callback: Callback to enhance session. 

65 

66 Returns: 

67 The registry with the given hostname 

68 """ 

69 if hostname == DOCKER_REGISTRY: 

70 hostname = DOCKER_HOSTNAME 

71 if hostname not in self._registries: 

72 self._registries[hostname] = Registry(hostname, session_callback) 

73 return self._registries[hostname] 

74 

75 def get_manifest(self, name: str | ReferenceParts) -> Manifest: 

76 """Get a manifest. 

77 

78 Args: 

79 name: Reference name, or reference parts. 

80 

81 Returns: 

82 The Manifest with the given full reference name. 

83 """ 

84 full_reference_parts = self.full_reference_parts(name) if isinstance(name, str) else name 

85 registry = self.get_registry(full_reference_parts.hostname) 

86 return Manifest( 

87 registry, 

88 full_reference_parts.manifest_name, 

89 full_reference_parts.digest, 

90 tag=full_reference_parts.tag or "latest", 

91 ) 

92 

93 @classmethod 

94 def short_reference(cls, name: str) -> str: 

95 """Get short reference (i.e. familiar name). 

96 

97 Args: 

98 name: Reference name. 

99 

100 Returns: 

101 The corresponding short reference name. 

102 """ 

103 short_reference = cls.full_reference(name) 

104 if short_reference.startswith(f"{DOCKER_HOSTNAME}/library/"): 

105 prefix_len = len(DOCKER_HOSTNAME) + 1 + len("library") + 1 

106 short_reference = short_reference[prefix_len:] 

107 if short_reference.startswith(f"{DOCKER_HOSTNAME}/"): 

108 prefix_len = len(DOCKER_HOSTNAME) + 1 

109 short_reference = short_reference[prefix_len:] 

110 return re_sub(":latest(@sha256:[0-9A-Fa-f]{64})?$", r"\1", short_reference) 

111 

112 @classmethod 

113 def full_reference(cls, name: str) -> str: 

114 """Get full reference. 

115 

116 Args: 

117 name: Reference name. 

118 

119 Returns: 

120 The corresponding full reference name. 

121 """ 

122 full_reference_parts = cls.full_reference_parts(name) 

123 full_reference = f"{full_reference_parts.hostname}/{full_reference_parts.manifest_name}" 

124 if full_reference_parts.tag: 

125 full_reference = f"{full_reference}:{full_reference_parts.tag}" 

126 if full_reference_parts.digest: 

127 full_reference = f"{full_reference}@{full_reference_parts.digest}" 

128 return full_reference 

129 

130 @classmethod 

131 def full_reference_parts(cls, name: str) -> ReferenceParts: 

132 """Get full reference parts (hostname, manifest_name, tag, digest). 

133 

134 Args: 

135 name: Reference name. 

136 

137 Returns: 

138 The corresponding full reference parts. 

139 """ 

140 hostname, remaining = cls._split_docker_domain(name) 

141 digest: str | None 

142 tag: str | None 

143 try: 

144 remaining, digest = remaining.split("@", 1) 

145 except ValueError: 

146 digest = None 

147 try: 

148 remaining, tag = remaining.split(":", 1) 

149 except ValueError: 

150 tag = None 

151 if hostname == DOCKER_HOSTNAME and "/" not in remaining: 

152 remaining = f"library/{remaining}" 

153 return ReferenceParts(hostname, remaining, tag, digest) 

154 

155 @classmethod 

156 def _split_docker_domain(cls, name: str) -> tuple[str, str]: 

157 parts = name.split("/", 1) 

158 if len(parts) == 2 and re_search(r"^localhost$|:\d|\.", parts[0]): # noqa: PLR2004 

159 return parts[0], parts[1] 

160 return DOCKER_HOSTNAME, name