Skip to content

Commit

Permalink
feat: support for OCI Image Manifest
Browse files Browse the repository at this point in the history
fix: #48
  • Loading branch information
eggplants committed Mar 20, 2023
1 parent 78c73c6 commit 8d7846a
Show file tree
Hide file tree
Showing 3 changed files with 39 additions and 30 deletions.
50 changes: 25 additions & 25 deletions ghcr_badge/dicts.py
Original file line number Diff line number Diff line change
@@ -1,50 +1,50 @@
"""Define objects of Image Manifest V 2, Schema 2 by TypedDict.
"""Define TypedDict classes of Image Manifest (V2-2) and OCI Image Manifest (V1).
See:
https://docs.docker.com/registry/spec/manifest-v2-2/#manifest-list-field-descriptions
https://github.com/opencontainers/image-spec/blob/main/manifest.md
"""

from __future__ import annotations

from typing import TypedDict


class ManifestV2(TypedDict):
"""Image Manifest root."""

mediaType: str # noqa: N815
schemaVersion: int # noqa: N815
config: ManifestV2Layer
layers: list[ManifestV2Layer]


class ManifestV2Layer(TypedDict):
"""Fields of an item in the layers list."""

class _ManifestV2Layer(TypedDict):
mediaType: str # noqa: N815
digest: str
size: int


class ManifestListV2(TypedDict):
"""The manifests field contains a list of manifests."""
class ManifestV2(TypedDict):
"""Image Manifest V2."""

mediaType: str # noqa: N815
schemaVersion: int # noqa: N815
manifests: list[ManifestV2Layer]
config: _ManifestV2Layer
layers: list[_ManifestV2Layer]

class ManifestListV2(TypedDict):
"""Manifest List V2."""

class ManifestListV2Layer(TypedDict):
"""The manifests field contains a list of manifests."""
mediaType: str # noqa: N815
schemaVersion: int # noqa: N815
manifests: list[_ManifestV2Layer]

class _OCIImageManifestV1Config(TypedDict):
mediaType: str # noqa: N815
digest: str
size: int
platform: ManifestListV2Platform
digest: str

class _OCIImageManifestV1Layer(TypedDict):
mediaType: str # noqa: N815
size: int
digest: str

class ManifestListV2Platform(TypedDict):
"""The platform object describes the platform."""
class OCIImageManifestV1(TypedDict):
"""Manifest V2 for OCI Image."""

architecture: str
os: str
schemaVersion: int # noqa: N815
mediaType: str # noqa: N815
config: _OCIImageManifestV1Config
layers: list[_OCIImageManifestV1Layer]
annotations: dict[str, str]
18 changes: 13 additions & 5 deletions ghcr_badge/generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from anybadge import Badge # type: ignore[import]
from humanfriendly import format_size, parse_size

from .dicts import ManifestListV2, ManifestV2
from .dicts import ManifestListV2, ManifestV2, OCIImageManifestV1

if TYPE_CHECKING:
from typing_extensions import Self
Expand Down Expand Up @@ -50,7 +50,8 @@ class InvalidMediaTypeError(Exception):
_MEDIA_TYPE_MANIFEST = "application/vnd.docker.distribution.manifest"
_MEDIA_TYPE_MANIFEST_V2 = f"{_MEDIA_TYPE_MANIFEST}.v2+json"
_MEDIA_TYPE_MANIFEST_LIST_V2 = f"{_MEDIA_TYPE_MANIFEST}.list.v2+json"

_MEDIA_TYPE_OCI_IMAGE_MANIFEST = "application/vnd.oci.image.manifest"
_MEDIA_TYPE_OCI_IMAGE_MANIFEST_V1 = f"{_MEDIA_TYPE_OCI_IMAGE_MANIFEST}.v1+json"

class GHCRBadgeGenerator:
"""Generator for GHCR Badge."""
Expand Down Expand Up @@ -248,7 +249,7 @@ def get_manifest(
package_name: str,
*,
tag: str = "latest",
) -> ManifestV2:
) -> ManifestV2 | OCIImageManifestV1:
"""Get manifest from ghcr api.
Parameters
Expand Down Expand Up @@ -283,7 +284,11 @@ class instance
url = f"https://ghcr.io/v2/{package_owner}/{package_name}/manifests/{tag}"
manifest = requests.get(
url,
headers={"User-Agent": _USER_AGENT, "Authorization": f"Bearer {token}"},
headers={
"User-Agent": _USER_AGENT,
"Authorization": f"Bearer {token}",
"Accept": f"{_MEDIA_TYPE_MANIFEST_V2},{_MEDIA_TYPE_OCI_IMAGE_MANIFEST_V1}",
},
timeout=_TIMEOUT,
).json()

Expand All @@ -295,8 +300,10 @@ class instance
msg = f"manifest contains some error: {manifest.get('errors')}"
raise InvalidManifestError(msg)

if (media_type := manifest.get("mediaType")) == _MEDIA_TYPE_MANIFEST_V2:
if (media_type := manifest.get("mediaType")) ==_MEDIA_TYPE_MANIFEST_V2:
return cast(ManifestV2, manifest)
if (media_type := manifest.get("mediaType")) ==_MEDIA_TYPE_OCI_IMAGE_MANIFEST_V1:
return cast(OCIImageManifestV1, manifest)

if media_type == _MEDIA_TYPE_MANIFEST_LIST_V2:
manifest = cast(ManifestListV2, manifest)
Expand All @@ -308,6 +315,7 @@ class instance
msg = f"Digest of a manifest is empty:\n{manifests[0]}"
raise InvalidManifestError(msg)
return self.get_manifest(package_owner, package_name, tag=digest)

raise InvalidMediaTypeError(media_type)

def get_tags(self: Self, package_owner: str, package_name: str) -> list[str]:
Expand Down
1 change: 1 addition & 0 deletions ghcr_badge/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ def get_index() -> Response:
"/ptr727/plexcleaner/develop_tag",
"/eggplants/ghcr-badge/size",
"/frysztak/orpington-news/size",
"/tuananh/aws-cli/size",
],
"repo": "https://github.com/eggplants/ghcr-badge",
"version": __version__,
Expand Down

0 comments on commit 8d7846a

Please sign in to comment.