Skip to content

Commit

Permalink
Add support for build metadata in version string
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
8R0WNI3 committed Jul 2, 2024
1 parent c587f10 commit 2dde084
Show file tree
Hide file tree
Showing 3 changed files with 71 additions and 7 deletions.
2 changes: 2 additions & 0 deletions ccc/oci.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ def oci_client(
install_logging_handler: bool=True,
cfg_factory=None,
http_connection_pool_size:int=16,
sanitise_tags: bool=False,
) -> oc.Client:
def base_api_lookup(image_reference):
registry_cfg = model.container_registry.find_config(
Expand Down Expand Up @@ -107,6 +108,7 @@ def base_api_lookup(image_reference):
credentials_lookup=credentials_lookup,
routes=routes,
session=session,
sanitise_tags=sanitise_tags,
)


Expand Down
53 changes: 46 additions & 7 deletions oci/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,12 +220,19 @@ def blob_url(self, image_reference: typing.Union[str, om.OciImageReference], dig
digest
)

def manifest_url(self, image_reference: typing.Union[str, om.OciImageReference]) -> str:
def manifest_url(
self,
image_reference: typing.Union[str, om.OciImageReference],
sanitise_tags: bool=False,
) -> str:
image_reference = om.OciImageReference.to_image_ref(image_reference)

if not (tag := image_reference.tag):
raise ValueError(f'{image_reference=} does not seem to contain a tag')

if sanitise_tags:
tag = oci.util.sanitise_tag(tag=tag)

return urljoin(
self.artifact_base_url(image_reference=image_reference),
'manifests',
Expand All @@ -248,10 +255,21 @@ def __init__(
self,
credentials_lookup: typing.Callable,
routes: OciRoutes=OciRoutes(),
disable_tls_validation=False,
disable_tls_validation: bool=False,
timeout_seconds: int=None,
session: requests.Session=None,
sanitise_tags: bool=False,
):
'''
@param credentials_lookup <Callable>
@param routes <OciRoutes>
@param disable_tls_validation <bool>
@param timeout_seconds <int>
@param session <Session>
@param sanitise_tags <bool>:
whether or not to sanitise tags so that they are accepted by OCI registries, see
`oci.util.sanitise_tag` for more details
'''
self.credentials_lookup = credentials_lookup
self.token_cache = OauthTokenCache()
if not session:
Expand All @@ -260,6 +278,7 @@ def __init__(
self.session = session
self.routes = routes
self.disable_tls_validation = disable_tls_validation
self.sanitise_tags = sanitise_tags

if timeout_seconds:
timeout_seconds = int(timeout_seconds)
Expand Down Expand Up @@ -510,7 +529,10 @@ def manifest_raw(

try:
res = self._request(
url=self.routes.manifest_url(image_reference=image_reference),
url=self.routes.manifest_url(
image_reference=image_reference,
sanitise_tags=self.sanitise_tags,
),
image_reference=image_reference,
scope=scope,
warn_if_not_ok=not absent_ok,
Expand Down Expand Up @@ -647,7 +669,10 @@ def head_manifest(
accept = om.MimeTypes.single_image

res = self._request(
url=self.routes.manifest_url(image_reference=image_reference),
url=self.routes.manifest_url(
image_reference=image_reference,
sanitise_tags=self.sanitise_tags,
),
image_reference=image_reference,
method='HEAD',
headers={
Expand Down Expand Up @@ -701,7 +726,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(
Expand Down Expand Up @@ -734,7 +767,10 @@ def put_manifest(
logger.debug(f'manifest-mimetype: {content_type=}')

res = self._request(
url=self.routes.manifest_url(image_reference=image_reference),
url=self.routes.manifest_url(
image_reference=image_reference,
sanitise_tags=self.sanitise_tags,
),
image_reference=image_reference,
scope=scope,
method='PUT',
Expand Down Expand Up @@ -782,7 +818,10 @@ def delete_manifest(
headers = {}

return self._request(
url=self.routes.manifest_url(image_reference=image_reference),
url=self.routes.manifest_url(
image_reference=image_reference,
sanitise_tags=self.sanitise_tags,
),
image_reference=image_reference,
scope=scope,
headers=headers,
Expand Down
23 changes: 23 additions & 0 deletions oci/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import threading
import typing

META_SEPARATOR = '.build-'


def normalise_image_reference(image_reference: str):
if not isinstance(image_reference, str):
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 2dde084

Please sign in to comment.