Skip to content

Commit 4f1b314

Browse files
committed
container source plugin supports watching update of a specified tag. Resolve #241
1 parent 0ba8cd4 commit 4f1b314

File tree

3 files changed

+93
-21
lines changed

3 files changed

+93
-21
lines changed

docs/usage.rst

+20-12
Original file line numberDiff line numberDiff line change
@@ -839,7 +839,9 @@ Check container registry
839839
This enables you to check tags of images on a container registry like Docker.
840840

841841
container
842-
The path for the container image. For official Docker images, use namespace ``library/`` (e.g. ``library/python``).
842+
The path (and tag) for the container image. For official Docker images, use namespace ``library/`` (e.g. ``library/python``).
843+
844+
If no tag is given, it checks latest available tag (sort by tag name), otherwise, it checks the tag's update time.
843845

844846
registry
845847
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
850852
omitted, use ``docker.io``, and if there is no slash in the path, prepend
851853
``library/`` to the path. Here are some examples:
852854

853-
+----------------------------------------------+-----------+--------------------------+
854-
| Pull command | registry | container |
855-
+==============================================+===========+==========================+
856-
| docker pull quay.io/prometheus/node-exporter | quay.io | prometheus/node-exporter |
857-
+----------------------------------------------+-----------+--------------------------+
858-
| docker pull nvidia/cuda | docker.io | nvidia/cuda |
859-
+----------------------------------------------+-----------+--------------------------+
860-
| docker pull python | docker.io | library/python |
861-
+----------------------------------------------+-----------+--------------------------+
862-
863-
This source returns tags and supports :ref:`list options`.
855+
+-----------------------------------------------------+-----------+---------------------------------+
856+
| Pull command | registry | container |
857+
+=====================================================+===========+=================================+
858+
| docker pull quay.io/prometheus/node-exporter | quay.io | prometheus/node-exporter |
859+
+-----------------------------------------------------+-----------+---------------------------------+
860+
| docker pull quay.io/prometheus/node-exporter:master | quay.io | prometheus/node-exporter:master |
861+
+-----------------------------------------------------+-----------+---------------------------------+
862+
| docker pull openeuler/openeuler | docker.io | openeuler/openeuler |
863+
+-----------------------------------------------------+-----------+---------------------------------+
864+
| docker pull openeuler/openeuler:20.03-lts | docker.io | openeuler/openeuler:20.03-lts |
865+
+-----------------------------------------------------+-----------+---------------------------------+
866+
| docker pull python | docker.io | library/python |
867+
+-----------------------------------------------------+-----------+---------------------------------+
868+
| docker pull python:3.11 | docker.io | library/python:3.11 |
869+
+-----------------------------------------------------+-----------+---------------------------------+
870+
871+
If no tag is given, this source returns tags and supports :ref:`list options`.
864872

865873
Check ALPM database
866874
~~~~~~~~~~~~~~~~~~~

nvchecker_source/container.py

+55-9
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from typing import Dict, List, NamedTuple, Optional, Tuple
55
from urllib.request import parse_http_list
66
from urllib.parse import urljoin
7+
import json
78

89
from nvchecker.api import session, HTTPError
910

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

5859
async def get_container_tags(info: Tuple[str, str, AuthInfo]) -> List[str]:
5960
image_path, registry_host, auth_info = info
60-
61-
auth_params = {
62-
'scope': f'repository:{image_path}:pull',
63-
}
64-
if auth_info.service:
65-
auth_params['service'] = auth_info.service
66-
res = await session.get(auth_info.realm, params=auth_params)
67-
token = res.json()['token']
68-
61+
token = await get_auth_token(auth_info, image_path)
6962
tags = []
7063
url = f'https://{registry_host}/v2/{image_path}/tags/list'
7164

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

8477
return tags
8578

79+
80+
async def get_auth_token(auth_info, image_path):
81+
auth_params = {
82+
'scope': f'repository:{image_path}:pull',
83+
}
84+
if auth_info.service:
85+
auth_params['service'] = auth_info.service
86+
res = await session.get(auth_info.realm, params=auth_params)
87+
token = res.json()['token']
88+
return token
89+
90+
8691
def parse_next_link(value: str) -> str:
8792
ending = '>; rel="next"'
8893
if value.endswith(ending):
8994
return value[1:-len(ending)]
9095
else:
9196
raise ValueError(value)
9297

98+
99+
async def get_container_tag_update_time(info: Tuple[str, str, str, AuthInfo]):
100+
'''
101+
Find the update time of a container tag.
102+
103+
In fact, it's the creation time of the image ID referred by the tag. Tag itself does not have any update time.
104+
'''
105+
image_path, image_tag, registry_host, auth_info = info
106+
token = await get_auth_token(auth_info, image_path)
107+
108+
# HTTP headers
109+
headers = {
110+
'Authorization': f'Bearer {token}',
111+
# Prefer Image Manifest Version 2, Schema 2: https://distribution.github.io/distribution/spec/manifest-v2-2/
112+
'Accept': 'application/vnd.docker.distribution.manifest.v2+json, application/vnd.docker.container.image.v1+json, application/json',
113+
}
114+
115+
# Get tag manifest
116+
url = f'https://{registry_host}/v2/{image_path}/manifests/{image_tag}'
117+
res = await session.get(url, headers=headers)
118+
data = res.json()
119+
# Schema 1 returns the creation time in the response
120+
if data['schemaVersion'] == 1:
121+
return json.loads(data['history'][0]['v1Compatibility'])['created']
122+
123+
# For schema 2, we have to fetch the config's blob
124+
digest = data['config']['digest']
125+
url = f'https://{registry_host}/v2/{image_path}/blobs/{digest}'
126+
res = await session.get(url, headers=headers)
127+
data = res.json()
128+
return data['created']
129+
130+
93131
async def get_version(name, conf, *, cache, **kwargs):
94132
image_path = conf.get('container', name)
133+
image_tag = None
134+
# image tag is optional
135+
if ':' in image_path:
136+
image_path, image_tag = image_path.split(':', 1)
95137
registry_host = conf.get('registry', 'docker.io')
96138
if registry_host == 'docker.io':
97139
registry_host = 'registry-1.docker.io'
98140

99141
auth_info = await cache.get(registry_host, get_registry_auth_info)
100142

143+
# if a tag is given, return the tag's update time, otherwise return the image's tag list
144+
if image_tag:
145+
key = image_path, image_tag, registry_host, auth_info
146+
return await cache.get(key, get_container_tag_update_time)
101147
key = image_path, registry_host, auth_info
102148
return await cache.get(key, get_container_tags)

tests/test_container.py

+18
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
# Copyright (c) 2020 Chih-Hsuan Yen <yan12125 at gmail dot com>
33

44
import pytest
5+
import datetime
56
pytestmark = [pytest.mark.asyncio, pytest.mark.needs_net]
67

78
async def test_container(get_version):
@@ -11,6 +12,23 @@ async def test_container(get_version):
1112
"include_regex": "linux",
1213
}) == "linux"
1314

15+
async def test_container_with_tag(get_version):
16+
update_time = await get_version("hello-world:linux", {
17+
"source": "container",
18+
"container": "library/hello-world:linux",
19+
})
20+
# the update time is changing occasionally, so we can not compare the exact time, otherwise the test will be failed in the future
21+
assert datetime.datetime.fromisoformat(update_time).date() > datetime.date(2023, 1, 1)
22+
23+
async def test_container_with_tag_and_registry(get_version):
24+
update_time = await get_version("hello-world-nginx:v1.0", {
25+
"source": "container",
26+
"registry": "quay.io",
27+
"container": "redhattraining/hello-world-nginx:v1.0",
28+
})
29+
# the update time probably won't be changed
30+
assert datetime.datetime.fromisoformat(update_time).date() == datetime.date(2019, 6, 26)
31+
1432
async def test_container_paging(get_version):
1533
assert await get_version("prometheus-operator", {
1634
"source": "container",

0 commit comments

Comments
 (0)