Skip to content

Commit 85d054d

Browse files
extension: extension: Improve to turn it more generic
Signed-off-by: Patrick José Pereira <patrickelectric@gmail.com>
1 parent 228036b commit 85d054d

1 file changed

Lines changed: 113 additions & 24 deletions

File tree

blueos_repository/extension/extension.py

Lines changed: 113 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import aiohttp
66
import json5
77
from docker.hub import DockerHub
8+
from docker.image_ref import DockerImageRef
89
from docker.models.blob import Blob
910
from docker.models.manifest import ImageManifest, ManifestFetch, ManifestPlatform
1011
from docker.models.repo import RepoInfo
@@ -48,9 +49,23 @@ def __init__(self, metadata: ExtensionMetadata) -> None:
4849
# Versions
4950
self.versions: Dict[str, ExtensionVersion] = {}
5051

51-
# Docker API
52-
self.hub: DockerHub = DockerHub(metadata.docker)
53-
self.registry: DockerRegistry = DockerRegistry(metadata.docker)
52+
# Parse docker image reference to determine registry
53+
self.image_ref: DockerImageRef = DockerImageRef.parse(metadata.docker)
54+
55+
# Docker Registry V2 API (works for any OCI-compliant registry)
56+
self.registry: DockerRegistry = DockerRegistry(
57+
self.image_ref.repository,
58+
registry_url=self.image_ref.registry_url,
59+
auth_url=self.image_ref.auth_url,
60+
auth_service=self.image_ref.auth_service,
61+
)
62+
63+
# Docker Hub has a proprietary REST API that provides richer tag
64+
# metadata (ordering, pull counts, per-image details). For other
65+
# registries we fall back to the standard V2 tag list on self.registry.
66+
self.docker_hub: Optional[DockerHub] = (
67+
DockerHub(self.image_ref.repository) if self.image_ref.is_dockerhub else None
68+
)
5469

5570
@property
5671
def sorted_versions(self) -> Dict[str, ExtensionVersion]:
@@ -101,6 +116,51 @@ def __extract_images_from_tag(tag: Tag) -> List[Image]:
101116
for image in active_images
102117
]
103118

119+
@staticmethod
120+
def __extract_images_from_manifest(manifest_fetch: ManifestFetch, blob: Blob) -> List[Image]:
121+
"""
122+
Derive per-platform image information directly from a registry manifest.
123+
124+
This is the registry-agnostic path used when the Docker Hub tag API is
125+
not available (e.g. GHCR, Quay, or any other OCI registry).
126+
127+
Args:
128+
manifest_fetch: The fetched manifest (may be a single image or a manifest list / OCI index).
129+
blob: The blob config for the embedded (ARM) image — used for
130+
platform info when the manifest is a single image.
131+
132+
Returns:
133+
List of Image objects.
134+
"""
135+
136+
if isinstance(manifest_fetch.manifest, ImageManifest):
137+
return [
138+
Image(
139+
digest=manifest_fetch.manifest.config.digest,
140+
expanded_size=sum(layer.size for layer in manifest_fetch.manifest.layers),
141+
platform=Platform(
142+
architecture=blob.architecture or "unknown",
143+
variant=None,
144+
os=blob.os or "unknown",
145+
),
146+
)
147+
]
148+
149+
# ManifestList / OCI Index — entry.size is the manifest document
150+
# size, NOT the image layer size, so we report 0 (unknown) instead.
151+
return [
152+
Image(
153+
digest=entry.digest,
154+
expanded_size=0,
155+
platform=Platform(
156+
architecture=entry.platform.architecture,
157+
variant=entry.platform.variant if entry.platform.variant else None,
158+
os=entry.platform.os if entry.platform.os else None,
159+
),
160+
)
161+
for entry in manifest_fetch.manifest.manifests
162+
]
163+
104164
def __is_compatible(self, platform: ManifestPlatform) -> bool:
105165
"""
106166
Checks if the platform is compatible with embedded devices BlueOS targets.
@@ -150,7 +210,25 @@ async def __extract_valid_embedded_digest(self, fetch: ManifestFetch) -> str:
150210

151211
raise RuntimeError(f"Expected to have a valid image manifest but got a manifest list: {manifest_fetch}")
152212

153-
async def __create_version_from_tag_blob(self, version_tag: Tag, blob: Blob) -> ExtensionVersion:
213+
async def __create_version(
214+
self,
215+
tag_name: str,
216+
blob: Blob,
217+
manifest: ManifestFetch,
218+
hub_tag: Optional[Tag] = None,
219+
) -> ExtensionVersion:
220+
"""
221+
Build an :class:`ExtensionVersion` from the blob labels, manifest, and
222+
(optionally) Docker Hub tag metadata.
223+
224+
Args:
225+
tag_name: The semver tag string (e.g. ``"1.2.3"``).
226+
blob: The config blob for the embedded ARM image.
227+
manifest: The top-level manifest fetch for this tag.
228+
hub_tag: If available, the Docker Hub ``Tag`` object that provides
229+
rich per-image metadata (sizes, architecture, status).
230+
"""
231+
154232
labels = blob.config.Labels
155233

156234
authors = labels.get("authors", "[]")
@@ -163,7 +241,7 @@ async def __create_version_from_tag_blob(self, version_tag: Tag, blob: Blob) ->
163241

164242
readme = labels.get("readme", None)
165243
if readme is not None:
166-
url = readme.replace(r"{tag}", version_tag.name)
244+
url = readme.replace(r"{tag}", tag_name)
167245
try:
168246
readme = await Extension.fetch_readme(url)
169247
try:
@@ -175,17 +253,23 @@ async def __create_version_from_tag_blob(self, version_tag: Tag, blob: Blob) ->
175253
Logger.warning(self.identifier, str(error))
176254
readme = "No README available"
177255

178-
images = self.__extract_images_from_tag(version_tag)
256+
# Prefer Docker Hub's rich per-image data when available; otherwise
257+
# derive the image list from the manifest (works for any registry).
258+
images: List[Image] = []
259+
if hub_tag:
260+
images = self.__extract_images_from_tag(hub_tag)
261+
if not images:
262+
images = self.__extract_images_from_manifest(manifest, blob)
179263
if not images:
180264
Logger.error(
181265
self.identifier,
182-
f"Could not find images associated with tag {version_tag.name} for extension {self.identifier}",
266+
f"Could not find images associated with tag {tag_name} for extension {self.identifier}",
183267
)
184268

185-
tag_identifier = str(uuid.uuid5(uuid.NAMESPACE_DNS, f"{self.identifier}.{version_tag.name}"))
269+
tag_identifier = str(uuid.uuid5(uuid.NAMESPACE_DNS, f"{self.identifier}.{tag_name}"))
186270
return ExtensionVersion(
187271
identifier=tag_identifier,
188-
tag=version_tag.name,
272+
tag=tag_name,
189273
type=ExtensionType(labels.get("type", ExtensionType.OTHER.value)),
190274
website=links.pop("website", labels.get("website", None)),
191275
readme=readme,
@@ -197,19 +281,19 @@ async def __create_version_from_tag_blob(self, version_tag: Tag, blob: Blob) ->
197281
docs=json5.loads(docs_raw) if docs_raw else None,
198282
company=json5.loads(company_raw) if company_raw else None,
199283
permissions=json5.loads(permissions_raw) if permissions_raw else None,
200-
images=self.__extract_images_from_tag(version_tag),
284+
images=images,
201285
)
202286

203-
async def __process_tag_version(self, tag: Tag) -> None:
287+
async def __process_tag(self, tag_name: str, hub_tag: Optional[Tag] = None) -> None:
204288
"""
205-
Process a tag and create a version object for it and store it in the versions
206-
dictionary property.
289+
Fetch the manifest and blob for *tag_name*, build an
290+
:class:`ExtensionVersion`, and store it in ``self.versions``.
207291
208292
Args:
209-
tag (Tag): Tag to process.
293+
tag_name: The tag string to process.
294+
hub_tag: Optional Docker Hub ``Tag`` object for richer metadata.
210295
"""
211296

212-
tag_name = tag.name
213297
try:
214298
if not valid_semver(tag_name):
215299
raise ValueError(f"Invalid version naming: {tag_name}")
@@ -220,7 +304,7 @@ async def __process_tag_version(self, tag: Tag) -> None:
220304
embedded_digest = await self.__extract_valid_embedded_digest(manifest)
221305
blob = await self.registry.get_manifest_blob(embedded_digest)
222306

223-
self.versions[tag_name] = await self.__create_version_from_tag_blob(tag, blob)
307+
self.versions[tag_name] = await self.__create_version(tag_name, blob, manifest, hub_tag)
224308

225309
Logger.info(self.identifier, f"Generated version entry {tag_name} for extension {self.identifier}")
226310
except ValueError as error:
@@ -251,15 +335,20 @@ async def inflate(self, tag: Optional[Tag] = None) -> None:
251335
"""
252336

253337
if tag:
254-
return await self.__process_tag_version(tag)
338+
return await self.__process_tag(tag.name, hub_tag=tag)
255339

256340
try:
257-
tags = await self.hub.get_tags()
258-
self.repo_info = await self.hub.repo_info()
341+
if self.docker_hub:
342+
# Docker Hub: use its proprietary API for ordered results,
343+
# rich per-image metadata, and download stats.
344+
hub_tags = await self.docker_hub.get_tags()
345+
self.repo_info = await self.docker_hub.repo_info()
346+
347+
await asyncio.gather(*(self.__process_tag(t.name, hub_tag=t) for t in hub_tags.results))
348+
else:
349+
# Any other OCI registry: use the standard V2 tag list.
350+
tag_names = await self.registry.list_tags()
351+
352+
await asyncio.gather(*(self.__process_tag(name) for name in tag_names))
259353
except Exception as error: # pylint: disable=broad-except
260354
Logger.error(self.identifier, f"Unable to fetch tags for {self.identifier}, error: {error}")
261-
return
262-
263-
# We may want to split and process first 5 tags prior to make sure we dont reach limit and always have the
264-
# latest ones processed.
265-
await asyncio.gather(*(self.__process_tag_version(tag) for tag in tags.results))

0 commit comments

Comments
 (0)