Skip to content

Commit 35a2bf5

Browse files
committed
Add OCI manifest compatibility checks to registry flow
1 parent 561d262 commit 35a2bf5

2 files changed

Lines changed: 54 additions & 11 deletions

File tree

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,3 +83,8 @@ docker pull junwha/ddiff-base:py3.10-torch2.4.1
8383

8484
# Requirements
8585
- Python 3.X
86+
87+
# Registry/API compatibility
88+
- Docker Registry HTTP API V2 endpoints are used (`/v2/<name>/manifests/<reference>`, `/v2/<name>/blobs/<digest>`).
89+
- Both Docker image manifest V2 and OCI image manifest V1 media types are supported.
90+
- OCI index / Docker manifest list (multi-arch index) is currently not supported yet.

ddiff.py

Lines changed: 49 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,25 @@
77
from pathlib import Path
88
import urllib
99
import urllib.request
10+
import urllib.parse
1011
import json
12+
import re
13+
14+
DOCKER_MANIFEST_V2 = "application/vnd.docker.distribution.manifest.v2+json"
15+
DOCKER_MANIFEST_LIST_V2 = "application/vnd.docker.distribution.manifest.list.v2+json"
16+
OCI_MANIFEST_V1 = "application/vnd.oci.image.manifest.v1+json"
17+
OCI_INDEX_V1 = "application/vnd.oci.image.index.v1+json"
18+
19+
SUPPORTED_MANIFEST_TYPES = [
20+
DOCKER_MANIFEST_V2,
21+
OCI_MANIFEST_V1,
22+
]
23+
24+
ACCEPT_MANIFEST_TYPES = ", ".join([
25+
*SUPPORTED_MANIFEST_TYPES,
26+
DOCKER_MANIFEST_LIST_V2,
27+
OCI_INDEX_V1,
28+
])
1129

1230
# Default values
1331
ddiff_port = os.getenv("DDIFF_PORT", "5000")
@@ -49,18 +67,29 @@ def _request_manifest(tag):
4967
manifest_url = f"{ddiff_url}/v2/{repo}/manifests/{version_tag}"
5068

5169
req = urllib.request.Request(manifest_url)
52-
req.add_header("Accept", "application/vnd.docker.distribution.manifest.v2+json")
70+
req.add_header("Accept", ACCEPT_MANIFEST_TYPES)
5371
try:
5472
with urllib.request.urlopen(req) as response:
5573
manifest = response.read().decode()
56-
return manifest
74+
content_type = (response.getheader("Content-Type") or "").split(";")[0]
75+
return manifest, content_type
5776
except urllib.error.HTTPError as e:
5877
print_error(f"HTTP error: {e.code} - {e.reason} ({manifest_url})")
5978
except Exception as e:
6079
print_error(f"Error: {e} ({manifest_url})")
6180

81+
def _validate_manifest_media_type(manifest):
82+
media_type = manifest.get("mediaType", "")
83+
if media_type in [DOCKER_MANIFEST_LIST_V2, OCI_INDEX_V1]:
84+
print_error("Manifest list/index is not supported yet. Please provide a single image manifest tag.")
85+
if media_type and media_type not in SUPPORTED_MANIFEST_TYPES:
86+
print_error(f"Unsupported manifest mediaType: {media_type}")
87+
return media_type
88+
89+
6290
def _parse_blob_list(manifest_str):
6391
manifest = json.loads(manifest_str)
92+
_validate_manifest_media_type(manifest)
6493
digests = []
6594

6695
# Config blob
@@ -75,7 +104,8 @@ def _parse_blob_list(manifest_str):
75104
return digests
76105

77106
def _download_blob(repo, digest, output_path):
78-
assert digest.startswith("sha256:") # Digest must start with 'sha256:'
107+
if not re.match(r"^[a-z0-9_+.-]+:[a-fA-F0-9]+$", digest):
108+
print_error(f"Invalid digest format: {digest}")
79109

80110
blob_url = f"{ddiff_url}/v2/{repo}/blobs/{digest}"
81111

@@ -105,7 +135,8 @@ def _upload_blob(repo, digest, blob_dir):
105135
# Load tar file of the layer
106136
with open(f"{blob_dir}/{digest}.tar", 'rb') as f:
107137
data = f.read()
108-
full_url = f"{session_url}&digest={digest}"
138+
delimiter = "&" if "?" in session_url else "?"
139+
full_url = f"{session_url}{delimiter}digest={urllib.parse.quote(digest)}"
109140
req = urllib.request.Request(full_url, data=data, method="PUT")
110141
req.add_header("Content-Type", "application/octet-stream")
111142
with urllib.request.urlopen(req) as res:
@@ -125,14 +156,14 @@ def _cross_mount(target_repo, base_repo, digest):
125156
except urllib.error.HTTPError as e:
126157
print_error(f"Mount failed: {e.code} {e.reason} ({url})")
127158

128-
def _upload_manifest(tag, manifest_path):
159+
def _upload_manifest(tag, manifest_path, manifest_media_type=DOCKER_MANIFEST_V2):
129160
repo, version_tag = tag.split(":")
130161
url = f"{ddiff_url}/v2/{repo}/manifests/{version_tag}"
131162
try:
132163
with open(manifest_path, 'rb') as f:
133164
data = f.read()
134165
req = urllib.request.Request(url, data=data, method="PUT")
135-
req.add_header("Content-Type", "application/vnd.docker.distribution.manifest.v2+json")
166+
req.add_header("Content-Type", manifest_media_type)
136167
with urllib.request.urlopen(req) as res:
137168
print_debug(f"Manifest uploaded to {repo} (status {res.status})")
138169
except urllib.error.HTTPError as e:
@@ -179,8 +210,8 @@ def diff_image(base_tag, target_tag):
179210
os.makedirs(blob_dir)
180211

181212
# Download manifest
182-
base_manifest = _request_manifest(base_tag)
183-
target_manifest = _request_manifest(target_tag)
213+
base_manifest, _ = _request_manifest(base_tag)
214+
target_manifest, target_manifest_media_type = _request_manifest(target_tag)
184215

185216
# Download different blobs
186217
base_blobs = _parse_blob_list(base_manifest)
@@ -194,6 +225,8 @@ def diff_image(base_tag, target_tag):
194225
# Write back metadata
195226
with open(output_dir + "/manifest.json", "w") as f:
196227
f.write(target_manifest)
228+
with open(os.path.join(output_dir, "MANIFEST_MEDIA_TYPE"), "w") as f:
229+
f.write(target_manifest_media_type)
197230
with open(os.path.join(output_dir, "BASE"), "w") as f:
198231
f.write(base_tag)
199232
with open(os.path.join(output_dir, "TARGET"), "w") as f:
@@ -234,6 +267,12 @@ def load_image(base_tag, image_tarball):
234267
with open(os.path.join(input_dir, "UPLOAD_BLOBS")) as f:
235268
upload_blobs = f.read().strip().split("|")
236269

270+
manifest_media_type = DOCKER_MANIFEST_V2
271+
manifest_media_type_path = os.path.join(input_dir, "MANIFEST_MEDIA_TYPE")
272+
if os.path.exists(manifest_media_type_path):
273+
with open(manifest_media_type_path) as f:
274+
manifest_media_type = f.read().strip() or DOCKER_MANIFEST_V2
275+
237276
# Mount
238277
for blob in mount_blobs:
239278
_cross_mount(target_repo, base_repo, blob)
@@ -242,7 +281,7 @@ def load_image(base_tag, image_tarball):
242281
for blob in upload_blobs:
243282
_upload_blob(target_repo, blob, blob_dir)
244283

245-
_upload_manifest(target_tag, f"{input_dir}/manifest.json")
284+
_upload_manifest(target_tag, f"{input_dir}/manifest.json", manifest_media_type)
246285

247286
shutil.rmtree(input_dir)
248287

@@ -282,7 +321,7 @@ def list_blobs(tag):
282321
tag = _prepare_tag(tag)
283322

284323
# Download manifest
285-
manifest = _request_manifest(tag)
324+
manifest, _ = _request_manifest(tag)
286325

287326

288327
# Check blobs
@@ -345,4 +384,3 @@ def list_blobs(tag):
345384
print("Usage: python3 ddiff.py list <tag>")
346385
sys.exit(1)
347386
list_blobs(args[0])
348-

0 commit comments

Comments
 (0)