Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

container source plugin supports watching update of a specified tag #243

Merged
merged 2 commits into from
Dec 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 20 additions & 12 deletions docs/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -839,7 +839,9 @@ Check container registry
This enables you to check tags of images on a container registry like Docker.

container
The path for the container image. For official Docker images, use namespace ``library/`` (e.g. ``library/python``).
The path (and tag) for the container image. For official Docker images, use namespace ``library/`` (e.g. ``library/python``).

If no tag is given, it checks latest available tag (sort by tag name), otherwise, it checks the tag's update time.

registry
The container registry host. Default: ``docker.io``
Expand All @@ -850,17 +852,23 @@ container name while this plugin requires the full name. If the host part is
omitted, use ``docker.io``, and if there is no slash in the path, prepend
``library/`` to the path. Here are some examples:

+----------------------------------------------+-----------+--------------------------+
| Pull command | registry | container |
+==============================================+===========+==========================+
| docker pull quay.io/prometheus/node-exporter | quay.io | prometheus/node-exporter |
+----------------------------------------------+-----------+--------------------------+
| docker pull nvidia/cuda | docker.io | nvidia/cuda |
+----------------------------------------------+-----------+--------------------------+
| docker pull python | docker.io | library/python |
+----------------------------------------------+-----------+--------------------------+

This source returns tags and supports :ref:`list options`.
+-----------------------------------------------------+-----------+---------------------------------+
| Pull command | registry | container |
+=====================================================+===========+=================================+
| docker pull quay.io/prometheus/node-exporter | quay.io | prometheus/node-exporter |
+-----------------------------------------------------+-----------+---------------------------------+
| docker pull quay.io/prometheus/node-exporter:master | quay.io | prometheus/node-exporter:master |
+-----------------------------------------------------+-----------+---------------------------------+
| docker pull openeuler/openeuler | docker.io | openeuler/openeuler |
+-----------------------------------------------------+-----------+---------------------------------+
| docker pull openeuler/openeuler:20.03-lts | docker.io | openeuler/openeuler:20.03-lts |
+-----------------------------------------------------+-----------+---------------------------------+
| docker pull python | docker.io | library/python |
+-----------------------------------------------------+-----------+---------------------------------+
| docker pull python:3.11 | docker.io | library/python:3.11 |
+-----------------------------------------------------+-----------+---------------------------------+

If no tag is given, this source returns tags and supports :ref:`list options`.

Check ALPM database
~~~~~~~~~~~~~~~~~~~
Expand Down
64 changes: 55 additions & 9 deletions nvchecker_source/container.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from typing import Dict, List, NamedTuple, Optional, Tuple
from urllib.request import parse_http_list
from urllib.parse import urljoin
import json

from nvchecker.api import session, HTTPError

Expand Down Expand Up @@ -57,15 +58,7 @@ async def get_registry_auth_info(registry_host: str) -> AuthInfo:

async def get_container_tags(info: Tuple[str, str, AuthInfo]) -> List[str]:
image_path, registry_host, auth_info = info

auth_params = {
'scope': f'repository:{image_path}:pull',
}
if auth_info.service:
auth_params['service'] = auth_info.service
res = await session.get(auth_info.realm, params=auth_params)
token = res.json()['token']

token = await get_auth_token(auth_info, image_path)
tags = []
url = f'https://{registry_host}/v2/{image_path}/tags/list'

Expand All @@ -83,20 +76,73 @@ async def get_container_tags(info: Tuple[str, str, AuthInfo]) -> List[str]:

return tags


async def get_auth_token(auth_info, image_path):
auth_params = {
'scope': f'repository:{image_path}:pull',
}
if auth_info.service:
auth_params['service'] = auth_info.service
res = await session.get(auth_info.realm, params=auth_params)
token = res.json()['token']
return token


def parse_next_link(value: str) -> str:
ending = '>; rel="next"'
if value.endswith(ending):
return value[1:-len(ending)]
else:
raise ValueError(value)


async def get_container_tag_update_time(info: Tuple[str, str, str, AuthInfo]):
'''
Find the update time of a container tag.

In fact, it's the creation time of the image ID referred by the tag. Tag itself does not have any update time.
'''
image_path, image_tag, registry_host, auth_info = info
token = await get_auth_token(auth_info, image_path)

# HTTP headers
headers = {
'Authorization': f'Bearer {token}',
# Prefer Image Manifest Version 2, Schema 2: https://distribution.github.io/distribution/spec/manifest-v2-2/
'Accept': 'application/vnd.docker.distribution.manifest.v2+json, application/vnd.docker.container.image.v1+json, application/json',
}

# Get tag manifest
url = f'https://{registry_host}/v2/{image_path}/manifests/{image_tag}'
res = await session.get(url, headers=headers)
data = res.json()
# Schema 1 returns the creation time in the response
if data['schemaVersion'] == 1:
return json.loads(data['history'][0]['v1Compatibility'])['created']

# For schema 2, we have to fetch the config's blob
digest = data['config']['digest']
url = f'https://{registry_host}/v2/{image_path}/blobs/{digest}'
res = await session.get(url, headers=headers)
data = res.json()
return data['created']


async def get_version(name, conf, *, cache, **kwargs):
image_path = conf.get('container', name)
image_tag = None
# image tag is optional
if ':' in image_path:
image_path, image_tag = image_path.split(':', 1)
registry_host = conf.get('registry', 'docker.io')
if registry_host == 'docker.io':
registry_host = 'registry-1.docker.io'

auth_info = await cache.get(registry_host, get_registry_auth_info)

# if a tag is given, return the tag's update time, otherwise return the image's tag list
if image_tag:
key = image_path, image_tag, registry_host, auth_info
return await cache.get(key, get_container_tag_update_time)
key = image_path, registry_host, auth_info
return await cache.get(key, get_container_tags)
18 changes: 18 additions & 0 deletions tests/test_container.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
# Copyright (c) 2020 Chih-Hsuan Yen <yan12125 at gmail dot com>

import pytest
import datetime
pytestmark = [pytest.mark.asyncio, pytest.mark.needs_net]

async def test_container(get_version):
Expand All @@ -11,6 +12,23 @@ async def test_container(get_version):
"include_regex": "linux",
}) == "linux"

async def test_container_with_tag(get_version):
update_time = await get_version("hello-world:linux", {
"source": "container",
"container": "library/hello-world:linux",
})
# the update time is changing occasionally, so we can not compare the exact time, otherwise the test will be failed in the future
assert datetime.date.fromisoformat(update_time.split('T')[0]) > datetime.date(2023, 1, 1)

async def test_container_with_tag_and_registry(get_version):
update_time = await get_version("hello-world-nginx:v1.0", {
"source": "container",
"registry": "quay.io",
"container": "redhattraining/hello-world-nginx:v1.0",
})
# the update time probably won't be changed
assert datetime.date.fromisoformat(update_time.split('T')[0]) == datetime.date(2019, 6, 26)

async def test_container_paging(get_version):
assert await get_version("prometheus-operator", {
"source": "container",
Expand Down
Loading