Skip to content

Commit 531b3eb

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

File tree

4 files changed

+67
-1081
lines changed

4 files changed

+67
-1081
lines changed

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

+5-1
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

+61-45
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
import glob
@@ -129,9 +132,6 @@ def main():
129132
singularity_rootfs = '/cvmfs/singularity.opensciencegrid.org'
130133
singularity_rootfs = os.path.abspath(singularity_rootfs)
131134

132-
# Does the registry require a token?
133-
doauth = not args.notoken
134-
135135
# Do we have a docker image specified?
136136
if not args.docker and not (args.filelist or args.filelist_path):
137137
print("No docker image or file list specified..", file=sys.stderr)
@@ -141,9 +141,9 @@ def main():
141141
if args.docker:
142142
image = args.docker
143143
if not args.dryrun:
144-
return publish_image(image, singularity_rootfs, args.registry, doauth, manifest_cache)
144+
return publish_image(image, singularity_rootfs, args.registry, manifest_cache)
145145
else:
146-
return verify_image(image, args.registry, doauth, manifest_cache)
146+
return verify_image(image, args.registry)
147147
else:
148148
final_retval = 0
149149
failed_images = []
@@ -162,7 +162,7 @@ def main():
162162

163163
if '*' in repo_tag: # Treat wildcards as a glob
164164
try:
165-
tag_names = get_tags(namespace, repo_name, registry=registry, auth=doauth)
165+
tag_names = get_tags(namespace, repo_name, registry=registry)
166166
except Exception as ex:
167167
image = '%s/%s/%s' % (registry, namespace, repo_name)
168168
print("Failed to get tags for image: {}".format(image))
@@ -190,7 +190,7 @@ def main():
190190
for i in range(tries):
191191
if not args.dryrun:
192192
try:
193-
retval = publish_image(image, singularity_rootfs, registry, doauth, manifest_cache)
193+
retval = publish_image(image, singularity_rootfs, registry, manifest_cache)
194194
except Exception as ex:
195195
if i < tries -1:
196196
print("Failed to publish image: {}".format(image))
@@ -201,7 +201,7 @@ def main():
201201
print("Tried {} times ".format(tries) + "for image {}".format(image) + ", giving up")
202202
else:
203203
try:
204-
retval = verify_image(image, registry, doauth, manifest_cache)
204+
retval = verify_image(image, registry)
205205
except Exception as ex:
206206
if i < tries -1:
207207
print("Failed to verify image: {}".format(image))
@@ -254,21 +254,56 @@ def start_txn(singularity_rootfs):
254254
if oe.errno != errno.EEXIST:
255255
raise
256256

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

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

357392
return registry, namespace, repo_name, repo_tag
358393

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

372396
# Tell the user the namespace, repo name and tag
373397
registry, namespace, repo_name, repo_tag = parse_image(image)
@@ -383,8 +407,7 @@ def publish_image(image, singularity_rootfs, registry, doauth, manifest_cache):
383407
if "://" not in registry:
384408
registry = "https://%s" % registry
385409
auth = DOCKER_CREDS.get(registry, {})
386-
hub = dockerhub.DockerHub(url=registry, namespace=namespace, repo=repo_name, **auth)
387-
manifest = get_manifest(hub, namespace, repo_name, repo_tag, manifest_cache)
410+
manifest = get_manifest(namespace, repo_name, repo_tag, registry=registry, cache=manifest_cache)
388411

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

462-
def verify_image(image, registry, doauth, manifest_cache):
485+
def verify_image(image, registry):
463486

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

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

0 commit comments

Comments
 (0)