From 232dd6b995721e6f3dbc7584eb23f75287a04573 Mon Sep 17 00:00:00 2001 From: "Jonas Brand (8R0WNI3)" Date: Mon, 1 Jul 2024 15:57:04 +0200 Subject: [PATCH] Add support for build metadata in version string To be compatible with SemVer, we should be able to handle versions which include optional build metadata ([see](https://semver.org/#spec-item-10)). In general, this is already the case. However, (some) OCI registries don't allow `+` in their tags, which is used as separator for these build metadata. That's why, the version has to be sanitised prior to uploading to a registry or after retrieving it from there. --- oci/client.py | 24 ++++++++++++++++++++++-- oci/util.py | 23 +++++++++++++++++++++++ 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/oci/client.py b/oci/client.py index a6deed20e1..6287c8530f 100644 --- a/oci/client.py +++ b/oci/client.py @@ -169,8 +169,10 @@ class OciRoutes: def __init__( self, base_api_url_lookup: typing.Callable[[str], str]=base_api_url, + sanitise_tags: bool=False, ): self.base_api_url_lookup = base_api_url_lookup + self.sanitise_tags = sanitise_tags def artifact_base_url( self, @@ -226,6 +228,9 @@ def manifest_url(self, image_reference: typing.Union[str, om.OciImageReference]) if not (tag := image_reference.tag): raise ValueError(f'{image_reference=} does not seem to contain a tag') + if self.sanitise_tags: + tag = oci.util.sanitise_tag(tag=tag) + return urljoin( self.artifact_base_url(image_reference=image_reference), 'manifests', @@ -251,6 +256,7 @@ def __init__( disable_tls_validation=False, timeout_seconds: int=None, session: requests.Session=None, + sanitise_tags: bool=False, ): self.credentials_lookup = credentials_lookup self.token_cache = OauthTokenCache() @@ -258,8 +264,14 @@ def __init__( self.session = requests.Session() else: self.session = session - self.routes = routes + if routes: + self.routes = routes + else: + self.routes = OciRoutes( + sanitise_tags=sanitise_tags, + ) self.disable_tls_validation = disable_tls_validation + self.sanitise_tags = sanitise_tags if timeout_seconds: timeout_seconds = int(timeout_seconds) @@ -701,7 +713,15 @@ def tags(self, image_reference: str): method='GET' ) - return res.json()['tags'] + tags = res.json()['tags'] + + if self.sanitise_tags: + tags = [ + oci.util.desanitise_tag(tag=tag) + for tag in tags + ] + + return tags def has_multiarch(self, image_reference: str) -> bool: res = self.head_manifest( diff --git a/oci/util.py b/oci/util.py index 877cbaed0f..16b1d492a2 100644 --- a/oci/util.py +++ b/oci/util.py @@ -4,6 +4,8 @@ import threading import typing +META_SEPARATOR = '.build-' + def normalise_image_reference(image_reference: str): if not isinstance(image_reference, str): @@ -42,6 +44,27 @@ def urljoin(*parts): return '/'.join([first] + middle + [last]) +def sanitise_tag(tag: str) -> str: + ''' + Additional build metadata as defined in SemVer can be added via `+` to the version. However, + OCI registries don't support `+` as tag character, which is why it has to be sanitised, for + example using `META_SEPARATOR`. + ''' + sanitised_tag = tag.replace('+', META_SEPARATOR) + + return sanitised_tag + + +def desanitise_tag(tag: str) -> str: + ''' + This function reverts the sanitisation of the `sanitise_tag` function, which allows processing + the tag the same way as prior to uploading to the OCI registry using `sanitise_tag`. + ''' + desanitised_tag = tag.replace(META_SEPARATOR, '+') + + return desanitised_tag + + class _TeeFilelikeProxy: ''' Takes a filelike object (which may be a non-seekable stream) and patches its