diff --git a/.github/actions/docker-images-verification/Dockerfile b/.github/actions/docker-images-verification/Dockerfile index a20b41d..50f288e 100644 --- a/.github/actions/docker-images-verification/Dockerfile +++ b/.github/actions/docker-images-verification/Dockerfile @@ -4,7 +4,11 @@ FROM python RUN curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py RUN python get-pip.py -RUN pip install requests furl sqlitedict +RUN pip install \ + furl \ + requests \ + python-dxf \ + sqlitedict # Copies your code file from your action repository to the filesystem path `/` of the container COPY entrypoint.sh /entrypoint.sh diff --git a/cvmfs-singularity-sync b/cvmfs-singularity-sync index da8ea1f..d907c27 100755 --- a/cvmfs-singularity-sync +++ b/cvmfs-singularity-sync @@ -34,12 +34,15 @@ import argparse import os import errno import fnmatch +import functools +import glob import json import urllib.request, urllib.error, urllib.parse import hashlib import traceback import subprocess -import dockerhub +from dxf import DXF +import furl import cleanup import sqlitedict import glob @@ -129,9 +132,6 @@ def main(): singularity_rootfs = '/cvmfs/singularity.opensciencegrid.org' singularity_rootfs = os.path.abspath(singularity_rootfs) - # Does the registry require a token? - doauth = not args.notoken - # Do we have a docker image specified? if not args.docker and not (args.filelist or args.filelist_path): print("No docker image or file list specified..", file=sys.stderr) @@ -141,9 +141,9 @@ def main(): if args.docker: image = args.docker if not args.dryrun: - return publish_image(image, singularity_rootfs, args.registry, doauth, manifest_cache) + return publish_image(image, singularity_rootfs, args.registry, manifest_cache) else: - return verify_image(image, args.registry, doauth, manifest_cache) + return verify_image(image, args.registry) else: final_retval = 0 failed_images = [] @@ -162,7 +162,7 @@ def main(): if '*' in repo_tag: # Treat wildcards as a glob try: - tag_names = get_tags(namespace, repo_name, registry=registry, auth=doauth) + tag_names = get_tags(namespace, repo_name, registry=registry) except Exception as ex: image = '%s/%s/%s' % (registry, namespace, repo_name) print("Failed to get tags for image: {}".format(image)) @@ -190,7 +190,7 @@ def main(): for i in range(tries): if not args.dryrun: try: - retval = publish_image(image, singularity_rootfs, registry, doauth, manifest_cache) + retval = publish_image(image, singularity_rootfs, registry, manifest_cache) except Exception as ex: if i < tries -1: print("Failed to publish image: {}".format(image)) @@ -201,7 +201,7 @@ def main(): print("Tried {} times ".format(tries) + "for image {}".format(image) + ", giving up") else: try: - retval = verify_image(image, registry, doauth, manifest_cache) + retval = verify_image(image, registry) except Exception as ex: if i < tries -1: print("Failed to verify image: {}".format(image)) @@ -254,21 +254,56 @@ def start_txn(singularity_rootfs): if oe.errno != errno.EEXIST: raise - -def get_tags(username, repo, registry=None, auth=None): - if registry != "registry.hub.docker.com": - if "://" not in registry: - registry = "https://%s" % registry - auth = DOCKER_CREDS.get(registry, {}) - hub = dockerhub.DockerHub(url=registry, namespace=username, repo=repo, **auth) +# REGISTRY ------------------------------------------------- +# Reuse dxf object if possible. A token can be reused for access to all tags. +@functools.lru_cache(maxsize=None) +def get_dxf(registry, repo): + return DXF(registry, repo, docker_auth) + +def docker_auth(dxf, response): + '''DXF auth handler, using DOCKER_CREDS global''' + origin = furl.furl(response.url).origin + authvars = DOCKER_CREDS.get(origin, {}) + dxf.authenticate(response=response, **authvars) + +def get_tags(namespace, repo_name, registry='registry.hub.docker.com'): + '''Retrieve tag list. This API call is uncounted.''' + repo = namespace + '/' + repo_name + #dxf = DXF(registry, repo, docker_auth) + dxf = get_dxf(registry, repo) + return dxf.list_aliases() + +def get_manifest(namespace, repo_name, repo_tag, cache={}, registry='registry.hub.docker.com'): + '''Retrieve Docker manifest. If uncached, this counts as an API call.''' + repo = namespace + '/' + repo_name + #dxf = DXF(registry, repo, docker_auth) + dxf = get_dxf(registry, repo) + digest = dxf_get_digest(dxf, repo_tag) + + if digest in cache: + return cache[digest], digest else: - auth = DOCKER_CREDS.get('https://registry.hub.docker.com', {}) - hub = dockerhub.DockerHub(**auth) - tag_names = [] - for tag in hub.tags(username, repo): - tag_names.append(tag['name']) - return tag_names + manifest = dxf.get_manifest(repo_tag) + cache[digest] = manifest + return manifest +def get_digest(namespace, repo_name, repo_tag, registry='registry.hub.docker.com'): + '''Retrieve docker-content-digest of the manifest blob. This API call is uncounted.''' + repo = namespace + '/' + repo_name + #dxf = DXF(registry, repo, docker_auth) + dxf = get_dxf(registry, repo) + return dxf_get_digest(dxf, repo_tag) + +def dxf_get_digest(dxf, repo_tag): + # Harbor returns 404 on HEAD of /v2/{repo_name}/manifests/{repo_tag} + # without the ACCEPT header + headers = { + 'ACCEPT': 'application/vnd.oci.image.manifest.v1+json', + } + ret = dxf._request('head', 'manifests/' + repo_tag, headers=headers) + return ret.headers['docker-content-digest'] + +# ---------------------------------------------------------- def publish_txn(): global _in_txn if _in_txn: @@ -356,18 +391,7 @@ def parse_image(image): return registry, namespace, repo_name, repo_tag -def get_manifest(hub, namespace, repo_name, repo_tag, manifest_cache): - metadata = hub.manifest(namespace, repo_name, repo_tag, head=True) - digest = metadata.headers['docker-content-digest'] - - if digest in manifest_cache: - return manifest_cache[digest] - else: - manifest = hub.manifest(namespace, repo_name, repo_tag) - manifest_cache[digest] = manifest - return manifest - -def publish_image(image, singularity_rootfs, registry, doauth, manifest_cache): +def publish_image(image, singularity_rootfs, registry, manifest_cache): # Tell the user the namespace, repo name and tag registry, namespace, repo_name, repo_tag = parse_image(image) @@ -383,8 +407,7 @@ def publish_image(image, singularity_rootfs, registry, doauth, manifest_cache): if "://" not in registry: registry = "https://%s" % registry auth = DOCKER_CREDS.get(registry, {}) - hub = dockerhub.DockerHub(url=registry, namespace=namespace, repo=repo_name, **auth) - manifest = get_manifest(hub, namespace, repo_name, repo_tag, manifest_cache) + manifest = get_manifest(namespace, repo_name, repo_tag, registry=registry, cache=manifest_cache) # Calculate a unique hash across all layers. We'll use that as the identifier # for the final image. @@ -459,7 +482,7 @@ def publish_image(image, singularity_rootfs, registry, doauth, manifest_cache): # Publish CVMFS as necessary. return publish_txn() -def verify_image(image, registry, doauth, manifest_cache): +def verify_image(image, registry): # Tell the user the namespace, repo name and tag registry, namespace, repo_name, repo_tag = parse_image(image) @@ -468,16 +491,9 @@ def verify_image(image, registry, doauth, manifest_cache): # IMAGE METADATA ------------------------------------------- # Use Docker Registry API (version 2.0) to get images ids, manifest - # Get an image manifest - has image ids to parse, and will be - # used later to get Cmd - # Prepend "https://" to the registry - if "://" not in registry: - registry = "https://%s" % registry - auth = DOCKER_CREDS.get(registry, {}) - hub = dockerhub.DockerHub(url=registry, namespace=namespace, repo=repo_name, **auth) retval = 0 try: - hub.manifest(namespace, repo_name, repo_tag, head=True) + get_digest(namespace, repo_name, repo_tag, registry=registry) print(repo_name + ":" + repo_tag + " manifest found") retval = 0 except Exception as ex: diff --git a/dockerhub.py b/dockerhub.py deleted file mode 100644 index 3b30298..0000000 --- a/dockerhub.py +++ /dev/null @@ -1,1034 +0,0 @@ -# MIT License -# -# Copyright (c) 2017 Daniel Sullivan (mumblepins) -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - - - - -import json - -import requests -from furl import furl -from requests.auth import AuthBase -import traceback -import re - - -class TimeoutError(Exception): - pass - - -class ConnectionError(Exception): - pass - - -class AuthenticationError(Exception): - pass - - -class DockerHubAuth(AuthBase): - def __init__(self, requests_post, api_url, username=None, password=None, token=None, delete_creds=False, scope=None): - """ - - Args: - requests_post (:py:meth:`DockerHub._do_requests_post`): - api_url (str): - username (str, optional): - password (str, optional): - token (str, optional): - delete_creds (bool, optional): - """ - self._token = None - self._username = None - self._password = None - self._api_url = api_url - self._requests_post = requests_post - if token is not None: - self._token = token - return - if username is not None and password is not None: - self._username = username - self._password = password - self._get_authorization_token() - if delete_creds: - self._username = None - self._password = None - return - - # Otherwise, do anonymous login - self._get_authorization_token() - #raise ValueError("Need either username and password or token for authentication") - - @property - def token(self): - return self._token - - def __eq__(self, other): - return self._token == getattr(other, '_token', None) - - def __ne__(self, other): - return not self == other - - def __call__(self, r): - r.headers['Authorization'] = "Bearer {}".format(self._token) - return r - - def updateToken(self, scope, service=None, realm=None, **kwargs): - if self._username: - auth = (self._username,self._password) - else: - auth = None - - if scope: - params = {'service': 'registry.docker.io', 'scope': scope} - else: - params = {'service': 'registry.docker.io'} - if service: - params['service'] = service - if realm: - r = requests.get(realm, params=params, auth=auth) - else: - r = requests.get("https://auth.docker.io/token", params=params, auth=auth) - try: - self._token = r.json()['token'] - except KeyError as ke: - print("Unable to get token from json") - print(r.json()) - raise ke - - def _get_authorization_token(self): - """Actually gets the authentication token - - Raises: - AuthenticationError: didn't login right - - """ - - if self._username == None and self._password == None and self._token == None: - - r = self._requests_post(self._api_url, noPage=True) - - else: - r = self._requests_post( - self._api_url, - { - "username": self._username, - "password": self._password - }) - - if not r.ok: - raise AuthenticationError("Error Status {}:\n{}".format(r.status_code, json.dumps(r.json(), indent=2))) - self._token = r.json()['token'] - - -def parse_url(url): - """Parses a url into the base url and the query params - - Args: - url (str): url with query string, or not - - Returns: - (str, `dict` of `lists`): url, query (dict of values) - """ - f = furl(url) - query = f.args - query = {a[0]: a[1] for a in query.listitems()} - f.remove(query=True).path.normalize() - url = f.url - - return url, query - - -def user_cleaner(user): - """Converts none or _ to library, makes username lowercase - - Args: - user (str): - - Returns: - str: cleaned username - - """ - if user == "_" or user == "": - return "library" - try: - return user.lower() - except AttributeError: - return user - - -class DockerHub(object): - """Actual class for making API calls - - Args: - username (str, optional): - password(str, optional): - token(str, optional): - url(str, optional): Url of api (https://hub.docker.com) - namespace(str, optional): Namespace of a docker image - repo(str, optional): Repo of the image - version(str, optional): Api version (v2) - delete_creds (bool, optional): Whether to delete password after logging in (default True) - return_lists (bool, optional): Whether to return a `generator` from calls that return multiple values - (False, default), or to return a simple `list` (True) - """ - - # - def __init__(self, username=None, password=None, token=None, url=None, namespace=None, repo=None, version='v2', delete_creds=True, - return_lists=False): - - self._version = version - self._url = '{0}/{1}'.format(url or 'https://hub.docker.com', self.version) - self._namespace = None - self._repo = None - self._session = requests.Session() - self._auth = None - self._token = None - self._username = None - self._password = None - self._return_lists = return_lists - self.login(username, password, token, namespace, repo, delete_creds) - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - self.close() - - def close(self): - self._session.close() - - # - - # - @property - def return_lists(self): - """Whether functions should return generators (False) or lists (True) - - Returns: - bool - - """ - return self._return_lists - - @return_lists.setter - def return_lists(self, value): - self._return_lists = value - - @property - def username(self): - if self._username is None and self.logged_in: - self._get_username() - return self._username - - @property - def logged_in(self): - return self.token is not None - - @property - def version(self): - return self._version - - @property - def url(self): - return self._url - - @property - def namespace(self): - return self._namespace - - @property - def repo(self): - return self._repo - - @property - def token(self): - return self._token - - @token.setter - def token(self, value): - self._token = value - self._get_username() - - # - - # - - def _do_request(self, method, address, **kwargs): - try: - if 'timeout' not in kwargs: - kwargs['timeout'] = (5, 15) - - if 'ttl' not in kwargs: - ttl = 1 - else: - ttl = kwargs['ttl'] - del kwargs['ttl'] - - if 'auth' not in kwargs: - kwargs['auth'] = self._auth - - if 'headers' not in kwargs: - kwargs['headers'] = {"Content-Type": "application/json"} - elif 'Content-Type' not in kwargs['headers']: - kwargs['headers']['Content-Type'] = "application/json" - - url, query = parse_url(address) - if query: - address = url - if 'params' in kwargs: - query.update(kwargs['params']) - kwargs['params'] = query - - resp = self._session.request(method, address, **kwargs) - #print(address) - #print(kwargs) - - except requests.exceptions.Timeout as e: - raise TimeoutError('Connection Timeout. Download failed: {0}'.format(e)) - except requests.exceptions.RequestException as e: - raise ConnectionError('Connection Error. Download failed: {0}'.format(e)) - else: - if resp.status_code == 401 and ttl > 0: - # Update the auth token with the scope, and try again - # Parse the Www-Authenticate line, looks like: - # Bearer realm="https://git.ligo.org/jwt/auth",service="container_registry",scope="repository:lscsoft/lalsuite/lalsuite-v6.53:pull",error="invalid_token" - reg=re.compile('(\w+)[=] ?"?([\w\:\/\.\-]+)"?') - values = dict(reg.findall(resp.headers['Www-Authenticate'])) - self._auth.updateToken(**values) - kwargs['ttl'] = ttl-1 - return self._do_request(method, address, **kwargs) - try: - resp.raise_for_status() - except: - try: - print(resp.json()) - except: - print(resp.content) - print(resp.headers) - raise - return resp - - def _do_requests_get(self, address, **kwargs): - if 'params' not in kwargs: - kwargs['params'] = {} - if 'perPage' not in kwargs['params'] and 'noPage' not in kwargs: - kwargs['params']['perPage'] = 100 - if 'noPage' in kwargs: - del kwargs['noPage'] - return self._do_request('GET', address, **kwargs) - - def _do_requests_head(self, address, **kwargs): - return self._do_request('HEAD', address, **kwargs) - - def _do_requests_post(self, address, json_data=None, **kwargs): - return self._do_request('POST', address, json=json_data, **kwargs) - - def _do_requests_put(self, address, json_data=None, **kwargs): - return self._do_request('PUT', address, json=json_data, **kwargs) - - def _do_requests_patch(self, address, json_data, **kwargs): - return self._do_request('PATCH', address, json=json_data, **kwargs) - - def _do_requests_delete(self, address, **kwargs): - return self._do_request('DELETE', address, **kwargs) - - def _iter_requests_get(self, address, **kwargs): - if self.return_lists: - return list(self._iter_requests_get_generator(address, **kwargs)) - return self._iter_requests_get_generator(address, **kwargs) - - def _iter_requests_get_generator(self, address, **kwargs): - _next = None - resp = self._do_requests_get(address, **kwargs) - - while True: - if _next: - resp = self._do_requests_get(_next) - # print _next - - resp = resp.json() - - for i in resp['results']: - yield i - - if resp['next']: - _next = resp['next'] - continue - return - - def _api_url(self, path): - return '{0}/{1}'.format(self.url, path) - - def _get_username(self): - if self.logged_in: - self._username = user_cleaner(self.logged_in_user()['username']) - else: - self._username = None - - # - - def login(self, username=None, password=None, token=None, namespace=None, repo=None, delete_creds=True): - """Logs into Docker hub and gets a token - - Either username and password or token should be specified - - Args: - username (str, optional): - password (str, optional): - token (str, optional): - namespace (str, optional): required if the registry is ghcr.io or hub.opensciencegrid.org - repo (str, optional): required if the registry is ghcr.io or hub.opensciencegrid.org - delete_creds (bool, optional): - - Returns: - - """ - - self._username = user_cleaner(username) - self._password = password - self._token = token - if token is not None: - # login with token - self._auth = DockerHubAuth(self._do_requests_post, self._api_url('users/login'), token=token) - elif username is not None and password is not None: - # login with user/pass - self._auth = DockerHubAuth(self._do_requests_post, self._api_url('users/login'), username=username, - password=password) - elif 'ghcr.io' in self.url: - self._auth = DockerHubAuth(self._do_requests_get, "https://ghcr.io/token?service=ghcr.io&scope=repository:"+namespace+"/"+repo+":pull") - elif 'hub.opensciencegrid.org' in self.url: - self._auth = DockerHubAuth(self._do_requests_get, "https://hub.opensciencegrid.org/service/token?service=harbor-registry&scope=repository:"+namespace+"/"+repo+":pull") - else: - self._auth = DockerHubAuth(self._do_requests_get, "https://auth.docker.io/token?service=registry.docker.io") - - if delete_creds: - self._password = None - - self._token = self._auth.token - - def comments(self, user, repository, **kwargs): - """ - - Args: - user: - repository: - **kwargs: - - Returns: - - """ - user = user_cleaner(user) - url = self._api_url('repositories/{0}/{1}/comments'.format(user, repository)) - return self._iter_requests_get(url, **kwargs) - - def repository(self, user, repository, **kwargs): - """ - - Args: - user: - repository: - **kwargs: - - Returns: - - """ - user = user_cleaner(user) - url = self._api_url('repositories/{0}/{1}'.format(user, repository)) - return self._do_requests_get(url, **kwargs).json() - - def repositories(self, user, **kwargs): - """ - - Args: - user: - **kwargs: - - Returns: - - """ - user = user_cleaner(user) - url = self._api_url('repositories/{0}'.format(user)) - return self._iter_requests_get(url, **kwargs) - - def repositories_starred(self, user, **kwargs): - """ - - Args: - user: - **kwargs: - - Returns: - - """ - user = user_cleaner(user) - url = self._api_url('users/{0}/repositories/starred'.format(user)) - return self._iter_requests_get(url, **kwargs) - - def tags(self, user, repository, **kwargs): - """ - - Args: - user: - repository: - **kwargs: - - Returns: - - """ - user = user_cleaner(user) - url = self._api_url('repositories/{0}/{1}/tags'.format(user, repository)) - self._auth = None - return self._iter_requests_get(url, **kwargs) - - def manifest(self, user, repository, tag, head=False, **kwargs): - """ - - Args: - user: - repository: - tag: - head (bool, optional): - **kwargs: - - Returns: - - """ - url = self._api_url('{0}/{1}/manifests/{2}'.format(user, repository, tag)) - - #Added support to retrieve oci images for hub.opensciencegrid.org - #https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/31127 - headers_has_accept = False - if 'headers' not in kwargs: - kwargs['headers'] = {'ACCEPT' : 'application/vnd.oci.image.manifest.v1+json'} - else: - for headers_key in kwargs['headers'].keys(): - if 'accept' in headers_key.casefold(): - headers_has_accept = True - kwargs['headers'][headers_key]+=', application/vnd.oci.image.manifest.v1+json' - break - if not headers_has_accept: - kwargs['headers']['ACCEPT'] = 'application/vnd.oci.image.manifest.v1+json' - - if head: - return self._do_requests_head(url, **kwargs) - else: - return self._do_requests_get(url, **kwargs).json() - - def user(self, user, **kwargs): - """ - - Args: - user: - **kwargs: - - Returns: - - """ - user = user_cleaner(user) - url = self._api_url('users/{0}'.format(user)) - return self._do_requests_get(url, **kwargs).json() - - # ------ Logged In Section - - def logged_in_user(self): - """ - - Returns: - - """ - return self._do_requests_get(self._api_url('user')).json() - - def add_collaborator(self, user, repository, collaborator): - """ - - Args: - user: - repository: - collaborator: - - Returns: - - """ - user = user_cleaner(user) - url = self._api_url('repositories/{}/{}/collaborators'.format(user, repository)) - return self._do_requests_post(url, { - "user": collaborator.lower() - }).json() - - def build_details(self, user, repository, code): - """ - - Args: - user: - repository: - code: - - Returns: - - """ - user = user_cleaner(user) - url = self._api_url('repositories/{}/{}/buildhistory/{}'.format(user, repository, code)) - return self._do_requests_get(url).json() - - def build_history(self, user, repository, **kwargs): - """ - - Args: - user: - repository: - **kwargs: - - Returns: - - """ - user = user_cleaner(user) - url = self._api_url('repositories/{}/{}/buildhistory'.format(user, repository)) - return self._iter_requests_get(url, **kwargs) - - def build_links(self, user, repository, **kwargs): - """ - - Args: - user: - repository: - **kwargs: - - Returns: - - """ - user = user_cleaner(user) - url = self._api_url('repositories/{}/{}/links'.format(user, repository)) - return self._iter_requests_get(url, **kwargs) - - def build_settings(self, user, repository): - """ - - Args: - user: - repository: - - Returns: - - """ - user = user_cleaner(user) - url = self._api_url('repositories/{}/{}/autobuild'.format(user, repository)) - return self._do_requests_get(url).json() - - def build_trigger(self, user, repository): - """ - - Args: - user: - repository: - - Returns: - - """ - user = user_cleaner(user) - url = self._api_url('repositories/{}/{}/buildtrigger'.format(user, repository)) - return self._do_requests_get(url).json() - - def build_trigger_history(self, user, repository, **kwargs): - """ - - Args: - user: - repository: - **kwargs: - - Returns: - - """ - user = user_cleaner(user) - url = self._api_url('repositories/{}/{}/buildtrigger/history'.format(user, repository)) - return self._iter_requests_get(url, **kwargs) - - def collaborators(self, user, repository, **kwargs): - """ - - Args: - user: - repository: - **kwargs: - - Returns: - - """ - user = user_cleaner(user) - url = self._api_url('repositories/{}/{}/collaborators'.format(user, repository)) - return self._iter_requests_get(url, **kwargs) - - def create_build_link(self, user, repository, to_repo): - """ - - Args: - user: - repository: - to_repo: - - Returns: - - """ - user = user_cleaner(user) - url = self._api_url('repositories/{}/{}/links'.format(user, repository)) - return self._do_requests_post(url, { - "to_repo": to_repo - }).json() - - def create_build_tag(self, user, repository, details): - """ - - Args: - user: - repository: - details: - - Returns: - - """ - user = user_cleaner(user) - url = self._api_url('repositories/{}/{}/autobuild/tags'.format(user, repository)) - return self._do_requests_post(url, { - 'isNew': True, - 'namespace': user, - 'repoName': repository, - 'name': details['name'] if 'name' in details else 'latest', - 'dockerfile_location': details['dockerfile_location'] if 'dockerfile_location' in details else '/', - 'source_type': details['source_type'] if 'source_type' in details else 'Branch', - 'source_name': details['source_name'] if 'source_name' in details else 'master' - }).json() - - def create_repository(self, user, repository, details): - """ - - Args: - user: - repository: - details: - - Returns: - - """ - user = user_cleaner(user) - url = self._api_url('repositories') - data = { - 'name': repository, - 'namespace': user, - } - details.update(data) - return self._do_requests_post(url, details).json() - - def create_automated_build(self, user, repository, details): - """ - - Args: - user: - repository: - details: - - Returns: - - """ - user = user_cleaner(user) - url = self._api_url('repositories/{}/{}/autobuild'.format(user, repository)) - data = { - 'name': repository, - 'namespace': user, - 'active': True, - 'dockerhub_repo_name': "{}/{}".format(user, repository) - } - - details.update(data) - return self._do_requests_post(url, details).json() - - def create_webhook(self, user, repository, webhook_name): - """ - - Args: - user: - repository: - webhook_name: - - Returns: - - """ - user = user_cleaner(user) - url = self._api_url('repositories/{}/{}/webhooks'.format(user, repository)) - data = { - 'name': webhook_name - } - return self._do_requests_post(url, data).json() - - def create_webhook_hook(self, user, repository, webhook_id, webhook_url): - """ - - Args: - user: - repository: - webhook_id: - webhook_url: - - Returns: - - """ - user = user_cleaner(user) - url = self._api_url('repositories/{}/{}/webhooks/{}/hooks'.format(user, repository, webhook_id)) - data = { - 'hook_url': webhook_url - } - return self._do_requests_post(url, data).json() - - def delete_build_link(self, user, repository, build_id): - """ - - Args: - user: - repository: - build_id: - - Returns: - boolean: returns true if successful delete call - - """ - user = user_cleaner(user) - url = self._api_url('repositories/{}/{}/links/{}'.format(user, repository, build_id)) - resp = self._do_requests_delete(url) - # print_response(resp) - return resp.status_code == 204 - - def delete_build_tag(self, user, repository, tag_id): - """ - - Args: - user: - repository: - tag_id: - - Returns: - boolean: returns true if successful delete call - - """ - user = user_cleaner(user) - url = self._api_url('repositories/{}/{}/autobuild/tags/{}'.format(user, repository, tag_id)) - resp = self._do_requests_delete(url) - return resp.status_code == 204 - - def delete_tag(self, user, repository, tag): - """ - - Args: - user: - repository: - tag: - - Returns: - boolean: returns true if successful delete call - - """ - user = user_cleaner(user) - url = self._api_url('repositories/{}/{}/tags/{}'.format(user, repository, tag)) - resp = self._do_requests_delete(url) - return resp.status_code == 204 - - def delete_collaborator(self, user, repository, collaborator): - """ - - Args: - user: - repository: - collaborator: - - Returns: - boolean: returns true if successful delete call - - """ - user = user_cleaner(user) - url = self._api_url('repositories/{}/{}/collaborators/{}'.format(user, repository, collaborator.lower())) - resp = self._do_requests_delete(url) - return resp.status_code in [200, 201, 202, 203, 204] - - def delete_repository(self, user, repository): - """ - - Args: - user: - repository: - - Returns: - boolean: returns true if successful delete call - - """ - user = user_cleaner(user) - url = self._api_url('repositories/{}/{}'.format(user, repository)) - resp = self._do_requests_delete(url) - # print_response(resp) - return resp.status_code in [200, 201, 202, 203, 204] - - def delete_webhook(self, user, repository, webhook_id): - """ - - Args: - user: - repository: - webhook_id: - - Returns: - boolean: returns true if successful delete call - - """ - user = user_cleaner(user) - url = self._api_url('repositories/{}/{}/webhooks/{}'.format(user, repository, webhook_id)) - resp = self._do_requests_delete(url) - # print_response(resp) - return resp.status_code in [200, 201, 202, 203, 204] - - def registry_settings(self): - """ - - Returns: - - """ - url = self._api_url('users/{}/registry-settings'.format(self.username)) - return self._do_requests_get(url).json() - - def set_build_tag(self, user, repository, build_id, details): - """ - - Args: - user: - repository: - build_id: - details: - - Returns: - - """ - user = user_cleaner(user) - url = self._api_url('repositories/{}/{}/autobuild/tags/{}'.format(user, repository, build_id)) - data = { - 'id': build_id, - 'name': 'latest', - 'dockerfile_location': '/', - 'source_type': 'Branch', - 'source_name': 'master' - } - data.update(details) - return self._do_requests_put(url, details).json() - - def set_repository_description(self, user, repository, descriptions): - """ - - Args: - user: - repository: - descriptions: - - Returns: - - """ - user = user_cleaner(user) - url = self._api_url('repositories/{}/{}'.format(user, repository)) - data = {} - if 'full' in descriptions: - data['full_description'] = descriptions['full'] - if 'short' in descriptions: - data['description'] = descriptions['short'] - if not data: - raise ValueError("Need either 'short' or 'full' description specified") - - return self._do_requests_patch(url, data).json() - - def star_repository(self, user, repository): - """ - - Args: - user: - repository: - - Returns: - boolean: returns true if successful - - """ - user = user_cleaner(user) - url = self._api_url('repositories/{}/{}/stars'.format(user, repository)) - resp = self._do_requests_post(url, {}) - # print_response(resp) - return resp.status_code in [200, 201, 202, 203, 204] - - def unstar_repository(self, user, repository): - """ - - Args: - user: - repository: - - Returns: - boolean: returns true if successful - - """ - user = user_cleaner(user) - url = self._api_url('repositories/{}/{}/stars'.format(user, repository)) - resp = self._do_requests_delete(url) - # print_response(resp) - return resp.status_code in [200, 201, 202, 203, 204] - - def trigger_build(self, user, repository, details): - """ - - Args: - user: - repository: - details: - - Returns: - - """ - user = user_cleaner(user) - url = self._api_url('repositories/{}/{}/autobuild/trigger-build'.format(user, repository)) - data = { - 'dockerfile_location': '/', - 'source_type': 'Branch', - 'source_name': 'master' - } - data.update(details) - return self._do_requests_post(url, data).json() - - def webhooks(self, user, repository, **kwargs): - """ - - Args: - user: - repository: - **kwargs: - - Returns: - - """ - user = user_cleaner(user) - url = self._api_url('repositories/{}/{}/webhooks'.format(user, repository)) - return self._iter_requests_get(url, **kwargs) - - -if __name__ == '__main__': - pass - -__all__ = ["DockerHub", "DockerHubAuth", "AuthenticationError", "ConnectionError", "TimeoutError"] diff --git a/requirements.txt b/requirements.txt index b926f62..d98f044 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -docker==2.0.0 furl +python-dxf requests sqlitedict