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
« 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/>.
17from __future__ import annotations
19from re import search as re_search
20from re import sub as re_sub
21from typing import TYPE_CHECKING, NamedTuple
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
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
31 from gitlabracadabra.containers.authenticated_session import AuthenticatedSession
34class ReferenceParts(NamedTuple):
35 hostname: str
36 manifest_name: str
37 tag: str | None
38 digest: str | None
41class Registries(metaclass=SingletonMeta):
42 """All registies by name."""
44 def __init__(self) -> None:
45 """All connected registries.
47 Intented to be used as a singleton.
48 """
49 self._registries: dict[str, Registry] = {}
51 def reset(self) -> None:
52 """Reset registry cache."""
53 self._registries = {}
55 def get_registry(
56 self,
57 hostname: str,
58 session_callback: Callable[[AuthenticatedSession], None] | None = None,
59 ) -> Registry:
60 """Get a registry connection.
62 Args:
63 hostname: fqdn of a registry.
64 session_callback: Callback to enhance session.
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]
75 def get_manifest(self, name: str | ReferenceParts) -> Manifest:
76 """Get a manifest.
78 Args:
79 name: Reference name, or reference parts.
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 )
93 @classmethod
94 def short_reference(cls, name: str) -> str:
95 """Get short reference (i.e. familiar name).
97 Args:
98 name: Reference name.
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)
112 @classmethod
113 def full_reference(cls, name: str) -> str:
114 """Get full reference.
116 Args:
117 name: Reference name.
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
130 @classmethod
131 def full_reference_parts(cls, name: str) -> ReferenceParts:
132 """Get full reference parts (hostname, manifest_name, tag, digest).
134 Args:
135 name: Reference name.
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)
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