diff --git a/.github/workflows/sync-releases.yml b/.github/workflows/sync-releases.yml index 55cf1cf6..f8f8d762 100644 --- a/.github/workflows/sync-releases.yml +++ b/.github/workflows/sync-releases.yml @@ -9,10 +9,17 @@ jobs: sync-releases: runs-on: ubuntu-latest steps: + - name: Check team membership + if: github.event_name == 'workflow_dispatch' + uses: PrestaShop/.github/.github/actions/team-guard@master + with: + team_slug: prestashop-sa + token: ${{ secrets.JARVIS_TOKEN }} + - name: Checkout the repo - uses: actions/checkout@v2 + uses: actions/checkout@v5 - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: 3.9 - name: Install dependencies diff --git a/prestashop_docker/docker_api.py b/prestashop_docker/docker_api.py index 6ac081ff..89b5c597 100644 --- a/prestashop_docker/docker_api.py +++ b/prestashop_docker/docker_api.py @@ -4,6 +4,7 @@ import requests import ssl import time +from urllib.parse import urljoin logger = logging.getLogger(__name__) ssl._create_default_https_context = ssl._create_unverified_context @@ -21,7 +22,8 @@ def __init__(self, cache, debug): @type debug: bool """ self.sleep_time = 1 - self.url = 'https://hub.docker.com/v2/repositories/' + self.auth_url = 'https://auth.docker.io/token' + self.registry_url = 'https://registry-1.docker.io/v2/' self.cache = cache self.is_debug = debug @@ -31,37 +33,66 @@ def __init__(self, cache, debug): def get_tags(self, image_name): """Generate return tags - @return: The json content - @rtype: dict + The registry API is used instead of the Docker Hub one because + Docker Hub refuses to paginate large tag lists for anonymous + requests, while the registry returns them in a single response. + @param image_name: Name of the image (e.g. library/php) + @type image_name: str + @return: The tags, as a list of {'name': } dicts + @rtype: list """ logger.debug( 'Processing request for tags' ) - data = self.execute( - self.url + image_name + '/tags?page_size=100' - ) + headers = {'Authorization': 'Bearer ' + self.get_token(image_name)} + request_url = self.registry_url + image_name + '/tags/list' + + tags = [] + while request_url is not None: + resp = self.execute(request_url, headers) + tags += resp.json()['tags'] + if 'next' in resp.links: + request_url = urljoin(request_url, resp.links['next']['url']) + else: + request_url = None + + return [{'name': name} for name in tags] + + def get_token(self, image_name): + """Get an anonymous pull token for the registry API + + @param image_name: Name of the image the token grants access to + @type image_name: str + @return: The token + @rtype: str + """ + # Tokens are short-lived, never serve one from the cache + with requests_cache.disabled(): + resp = self.execute( + self.auth_url + '?service=registry.docker.io&scope=repository:' + image_name + ':pull' + ) - return data['results'] + return resp.json()['token'] - def execute(self, request_url): + def execute(self, request_url, headers=None): """Execute url @param request_url: The url to execute + @param headers: Optional HTTP headers @return: The HTTP Response - @rtype: dict + @rtype: requests.Response """ logger.debug( 'Execute URL: ' + request_url ) resp = requests.get( - request_url + request_url, + headers=headers ) - data = resp.json() - if resp.status_code != 200: # Something went wrong, retry time.sleep(self.sleep_time) @@ -69,16 +100,11 @@ def execute(self, request_url): if DockerApi.retries >= 10: raise requests.HTTPError(resp.text) - return self.execute(request_url) - else: - DockerApi.retries = 0 - # Data not in cache - if not hasattr(resp, 'from_cache') or not resp.from_cache: - time.sleep(self.sleep_time) - - if 'next' in data and data['next'] is not None: - # Compute items if there is a next url - data['results'] += self.execute( - data['next'] - )['results'] - return data + return self.execute(request_url, headers) + + DockerApi.retries = 0 + # Data not in cache + if not hasattr(resp, 'from_cache') or not resp.from_cache: + time.sleep(self.sleep_time) + + return resp