Skip to content

Commit df15ce8

Browse files
authored
Add example for pulling and following index images (#109)
* Add examples/follow-image-index.py Signed-off-by: Mike Richards <[email protected]>
1 parent 07272ea commit df15ce8

File tree

2 files changed

+90
-0
lines changed

2 files changed

+90
-0
lines changed

examples/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ The directory here has the following examples:
66

77
- [simple](simple): simple examples for individual commands adopted from oras-py [before the client was removed](https://github.com/oras-project/oras-py/tree/3b4e6d74d49b8c6a5d8180e646d52fcc50b3508a).
88
- [conda-mirror.py](conda-mirror.py): upload to a conda mirror with ORAS with a manifest and custom content types.
9+
- [follow-image-index.py](follow-image-index.py): Download a homebrew image index and select a platform-specific image.
910

1011
## In the Wild Examples
1112

examples/follow-image-index.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
"""
2+
Follow homebrew image index to get the 'hello' bottle specific to your platform
3+
"""
4+
import re
5+
6+
import oras.client
7+
import oras.provider
8+
from oras import decorator
9+
10+
11+
class MyRegistry(oras.provider.Registry):
12+
"""
13+
Oras registry with support for image indexes.
14+
"""
15+
16+
@decorator.ensure_container
17+
def get_image_index(self, container, allowed_media_type=None):
18+
"""
19+
Get an image index as a manifest.
20+
21+
This is basically Registry.get_manifest with the following changes
22+
23+
- different default allowed_media_type
24+
- no JSON schema validation
25+
"""
26+
if not allowed_media_type:
27+
default_image_index_media_type = "application/vnd.oci.image.index.v1+json"
28+
allowed_media_type = [default_image_index_media_type]
29+
30+
headers = {"Accept": ";".join(allowed_media_type)}
31+
32+
manifest_url = f"{self.prefix}://{container.manifest_url()}"
33+
response = self.do_request(manifest_url, "GET", headers=headers)
34+
self._check_200_response(response)
35+
manifest = response.json()
36+
# this would be a good point to validate the schema of the manifest
37+
# jsonschema.validate(manifest, schema=...)
38+
return manifest
39+
40+
41+
def get_uri_for_digest(uri, digest):
42+
"""
43+
Given a URI for an image, return a URI for the related digest.
44+
45+
URI may be in any of the following forms:
46+
47+
ghcr.io/homebrew/core/hello
48+
ghcr.io/homebrew/core/hello:2.10
49+
ghcr.io/homebrew/core/hello@sha256:ff81...47a
50+
"""
51+
base_uri = re.split(r"[@:]", uri, maxsplit=1)[0]
52+
return f"{base_uri}@{digest}"
53+
54+
55+
def get_image_for_platform(client, uri, download_to, platform_details):
56+
def matches_platform(manifest):
57+
platform = manifest.get("platform", {})
58+
return all(
59+
platform.get(key) == requested_value
60+
for key, requested_value in platform_details.items()
61+
)
62+
63+
index_manifest = client.remote.get_image_index(container=uri)
64+
# use first compatible manifest. YMMV and a tie-breaker may be more suitable
65+
for manifest in index_manifest["manifests"]:
66+
if matches_platform(manifest):
67+
break
68+
else:
69+
raise RuntimeError(
70+
f"No manifest definition matched platform {platform_details}"
71+
)
72+
73+
platform_image_uri = get_uri_for_digest(uri, manifest["digest"])
74+
client.pull(target=platform_image_uri, outdir=download_to)
75+
76+
77+
if __name__ == "__main__":
78+
client = oras.client.OrasClient(registry=MyRegistry())
79+
platform_details = {
80+
"architecture": "amd64",
81+
"os": "darwin",
82+
"os.version": "macOS 10.14",
83+
}
84+
get_image_for_platform(
85+
client,
86+
"ghcr.io/homebrew/core/hello:2.10",
87+
download_to="downloads",
88+
platform_details=platform_details,
89+
)

0 commit comments

Comments
 (0)