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

container source plugin supports watching update of a specified tag. Resolve #241
This commit is contained in:
Bian Jiaping 2023-12-02 23:45:25 +08:00 committed by GitHub
parent 0ba8cd41de
commit 4833135f87
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 92 additions and 20 deletions

View File

@ -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``
@ -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 |
+----------------------------------------------+-----------+--------------------------+
+-----------------------------------------------------+-----------+---------------------------------+
| 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 |
+-----------------------------------------------------+-----------+---------------------------------+
This source returns tags and supports :ref:`list options`.
If no tag is given, this source returns tags and supports :ref:`list options`.
Check ALPM database
~~~~~~~~~~~~~~~~~~~

View File

@ -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
@ -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'
@ -83,6 +76,18 @@ 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):
@ -90,13 +95,54 @@ def parse_next_link(value: str) -> str:
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)

View File

@ -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):
@ -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",