Skip to content

Commit afeb8a4

Browse files
committed
Switch to python-dxf for registry access
Retrieves tag list through an API call that's not rate limited
1 parent eb6ae26 commit afeb8a4

File tree

4 files changed

+67
-1081
lines changed

4 files changed

+67
-1081
lines changed

.github/actions/docker-images-verification/Dockerfile

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@ FROM python
44

55
RUN curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py
66
RUN python get-pip.py
7-
RUN pip install requests furl sqlitedict
7+
RUN pip install \
8+
furl \
9+
requests \
10+
python-dxf \
11+
sqlitedict
812
# Copies your code file from your action repository to the filesystem path `/` of the container
913

1014
COPY entrypoint.sh /entrypoint.sh

cvmfs-singularity-sync

Lines changed: 61 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,15 @@ import argparse
3434
import os
3535
import errno
3636
import fnmatch
37+
import functools
38+
import glob
3739
import json
3840
import urllib.request, urllib.error, urllib.parse
3941
import hashlib
4042
import traceback
4143
import subprocess
42-
import dockerhub
44+
from dxf import DXF
45+
import furl
4346
import cleanup
4447
import sqlitedict
4548

@@ -128,9 +131,6 @@ def main():
128131
singularity_rootfs = '/cvmfs/singularity.opensciencegrid.org'
129132
singularity_rootfs = os.path.abspath(singularity_rootfs)
130133

131-
# Does the registry require a token?
132-
doauth = not args.notoken
133-
134134
# Do we have a docker image specified?
135135
if not args.docker and not (args.filelist or args.filelist_path):
136136
print("No docker image or file list specified..", file=sys.stderr)
@@ -140,9 +140,9 @@ def main():
140140
if args.docker:
141141
image = args.docker
142142
if not args.dryrun:
143-
return publish_image(image, singularity_rootfs, args.registry, doauth, manifest_cache)
143+
return publish_image(image, singularity_rootfs, args.registry, manifest_cache)
144144
else:
145-
return verify_image(image, args.registry, doauth, manifest_cache)
145+
return verify_image(image, args.registry)
146146
else:
147147
final_retval = 0
148148
failed_images = []
@@ -161,7 +161,7 @@ def main():
161161

162162
if '*' in repo_tag: # Treat wildcards as a glob
163163
try:
164-
tag_names = get_tags(namespace, repo_name, registry=registry, auth=doauth)
164+
tag_names = get_tags(namespace, repo_name, registry=registry)
165165
except Exception as ex:
166166
image = '%s/%s/%s' % (registry, namespace, repo_name)
167167
print("Failed to get tags for image: {}".format(image))
@@ -189,7 +189,7 @@ def main():
189189
for i in range(tries):
190190
if not args.dryrun:
191191
try:
192-
retval = publish_image(image, singularity_rootfs, registry, doauth, manifest_cache)
192+
retval = publish_image(image, singularity_rootfs, registry, manifest_cache)
193193
except Exception as ex:
194194
if i < tries -1:
195195
print("Failed to publish image: {}".format(image))
@@ -200,7 +200,7 @@ def main():
200200
print("Tried {} times ".format(tries) + "for image {}".format(image) + ", giving up")
201201
else:
202202
try:
203-
retval = verify_image(image, registry, doauth, manifest_cache)
203+
retval = verify_image(image, registry)
204204
except Exception as ex:
205205
if i < tries -1:
206206
print("Failed to verify image: {}".format(image))
@@ -253,21 +253,56 @@ def start_txn(singularity_rootfs):
253253
if oe.errno != errno.EEXIST:
254254
raise
255255

256-
257-
def get_tags(username, repo, registry=None, auth=None):
258-
if registry != "registry.hub.docker.com":
259-
if "://" not in registry:
260-
registry = "https://%s" % registry
261-
auth = DOCKER_CREDS.get(registry, {})
262-
hub = dockerhub.DockerHub(url=registry, namespace=username, repo=repo, **auth)
256+
# REGISTRY -------------------------------------------------
257+
# Reuse dxf object if possible. A token can be reused for access to all tags.
258+
@functools.lru_cache(maxsize=None)
259+
def get_dxf(registry, repo):
260+
return DXF(registry, repo, docker_auth)
261+
262+
def docker_auth(dxf, response):
263+
'''DXF auth handler, using DOCKER_CREDS global'''
264+
origin = furl.furl(response.url).origin
265+
authvars = DOCKER_CREDS.get(origin, {})
266+
dxf.authenticate(response=response, **authvars)
267+
268+
def get_tags(namespace, repo_name, registry='registry.hub.docker.com'):
269+
'''Retrieve tag list. This API call is uncounted.'''
270+
repo = namespace + '/' + repo_name
271+
#dxf = DXF(registry, repo, docker_auth)
272+
dxf = get_dxf(registry, repo)
273+
return dxf.list_aliases()
274+
275+
def get_manifest(namespace, repo_name, repo_tag, cache={}, registry='registry.hub.docker.com'):
276+
'''Retrieve Docker manifest. If uncached, this counts as an API call.'''
277+
repo = namespace + '/' + repo_name
278+
#dxf = DXF(registry, repo, docker_auth)
279+
dxf = get_dxf(registry, repo)
280+
digest = dxf_get_digest(dxf, repo_tag)
281+
282+
if digest in cache:
283+
return cache[digest], digest
263284
else:
264-
auth = DOCKER_CREDS.get('https://registry.hub.docker.com', {})
265-
hub = dockerhub.DockerHub(**auth)
266-
tag_names = []
267-
for tag in hub.tags(username, repo):
268-
tag_names.append(tag['name'])
269-
return tag_names
285+
manifest = dxf.get_manifest(repo_tag)
286+
cache[digest] = manifest
287+
return manifest
270288

289+
def get_digest(namespace, repo_name, repo_tag, registry='registry.hub.docker.com'):
290+
'''Retrieve docker-content-digest of the manifest blob. This API call is uncounted.'''
291+
repo = namespace + '/' + repo_name
292+
#dxf = DXF(registry, repo, docker_auth)
293+
dxf = get_dxf(registry, repo)
294+
return dxf_get_digest(dxf, repo_tag)
295+
296+
def dxf_get_digest(dxf, repo_tag):
297+
# Harbor returns 404 on HEAD of /v2/{repo_name}/manifests/{repo_tag}
298+
# without the ACCEPT header
299+
headers = {
300+
'ACCEPT': 'application/vnd.oci.image.manifest.v1+json',
301+
}
302+
ret = dxf._request('head', 'manifests/' + repo_tag, headers=headers)
303+
return ret.headers['docker-content-digest']
304+
305+
# ----------------------------------------------------------
271306
def publish_txn():
272307
global _in_txn
273308
if _in_txn:
@@ -355,18 +390,7 @@ def parse_image(image):
355390

356391
return registry, namespace, repo_name, repo_tag
357392

358-
def get_manifest(hub, namespace, repo_name, repo_tag, manifest_cache):
359-
metadata = hub.manifest(namespace, repo_name, repo_tag, head=True)
360-
digest = metadata.headers['docker-content-digest']
361-
362-
if digest in manifest_cache:
363-
return manifest_cache[digest]
364-
else:
365-
manifest = hub.manifest(namespace, repo_name, repo_tag)
366-
manifest_cache[digest] = manifest
367-
return manifest
368-
369-
def publish_image(image, singularity_rootfs, registry, doauth, manifest_cache):
393+
def publish_image(image, singularity_rootfs, registry, manifest_cache):
370394

371395
# Tell the user the namespace, repo name and tag
372396
registry, namespace, repo_name, repo_tag = parse_image(image)
@@ -382,8 +406,7 @@ def publish_image(image, singularity_rootfs, registry, doauth, manifest_cache):
382406
if "://" not in registry:
383407
registry = "https://%s" % registry
384408
auth = DOCKER_CREDS.get(registry, {})
385-
hub = dockerhub.DockerHub(url=registry, namespace=namespace, repo=repo_name, **auth)
386-
manifest = get_manifest(hub, namespace, repo_name, repo_tag, manifest_cache)
409+
manifest = get_manifest(namespace, repo_name, repo_tag, registry=registry, cache=manifest_cache)
387410

388411
# Calculate a unique hash across all layers. We'll use that as the identifier
389412
# for the final image.
@@ -458,7 +481,7 @@ def publish_image(image, singularity_rootfs, registry, doauth, manifest_cache):
458481
# Publish CVMFS as necessary.
459482
return publish_txn()
460483

461-
def verify_image(image, registry, doauth, manifest_cache):
484+
def verify_image(image, registry):
462485

463486
# Tell the user the namespace, repo name and tag
464487
registry, namespace, repo_name, repo_tag = parse_image(image)
@@ -467,16 +490,9 @@ def verify_image(image, registry, doauth, manifest_cache):
467490
# IMAGE METADATA -------------------------------------------
468491
# Use Docker Registry API (version 2.0) to get images ids, manifest
469492

470-
# Get an image manifest - has image ids to parse, and will be
471-
# used later to get Cmd
472-
# Prepend "https://" to the registry
473-
if "://" not in registry:
474-
registry = "https://%s" % registry
475-
auth = DOCKER_CREDS.get(registry, {})
476-
hub = dockerhub.DockerHub(url=registry, namespace=namespace, repo=repo_name, **auth)
477493
retval = 0
478494
try:
479-
hub.manifest(namespace, repo_name, repo_tag, head=True)
495+
get_digest(namespace, repo_name, repo_tag, registry=registry)
480496
print(repo_name + ":" + repo_tag + " manifest found")
481497
retval = 0
482498
except Exception as ex:

0 commit comments

Comments
 (0)