Skip to content

Commit 6120c57

Browse files
committed
suite: Check for existence of container images
As opposed to checking for a specific completed package build, and inferring the status of the container based on the result. Signed-off-by: Zack Cerza <zack@cerza.org>
1 parent b056b94 commit 6120c57

4 files changed

Lines changed: 130 additions & 8 deletions

File tree

teuthology/suite/test/test_util.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,9 @@ class TestMissingPackages(object):
216216
scheduled job will have missing packages in shaman.
217217
"""
218218
@patch("teuthology.packaging.ShamanProject._get_package_version")
219-
def test_distro_has_packages(self, m_gpv):
219+
@patch("teuthology.suite.util.container_image_for_hash")
220+
def test_distro_has_packages(self, m_cifh, m_gpv):
221+
m_cifh.return_value = "xxx"
220222
m_gpv.return_value = "v1"
221223
result = util.package_version_for_hash(
222224
"sha1",
@@ -228,7 +230,9 @@ def test_distro_has_packages(self, m_gpv):
228230
assert result
229231

230232
@patch("teuthology.packaging.ShamanProject._get_package_version")
231-
def test_distro_does_not_have_packages(self, m_gpv):
233+
@patch("teuthology.suite.util.container_image_for_hash")
234+
def test_distro_does_not_have_packages(self, m_cifh, m_gpv):
235+
m_cifh.return_value = None
232236
m_gpv.return_value = None
233237
result = util.package_version_for_hash(
234238
"sha1",

teuthology/suite/util.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,10 @@
2222
from teuthology.packaging import get_builder_project, VersionNotFoundError
2323
from teuthology.repo_utils import build_git_url
2424
from teuthology.task.install import get_flavor
25+
from teuthology.util.containers import container_image_for_hash
2526

2627
log = logging.getLogger(__name__)
2728

28-
CONTAINER_DISTRO = 'centos/9' # the one to check for build_complete
29-
CONTAINER_FLAVOR = 'default'
30-
3129

3230
def fetch_repos(branch, test_name, dry_run, commit=None):
3331
"""
@@ -245,8 +243,7 @@ def package_version_for_hash(hash, flavor='default', distro='rhel',
245243
),
246244
)
247245

248-
if (bp.distro == CONTAINER_DISTRO and bp.flavor == CONTAINER_FLAVOR and
249-
not bp.build_complete):
246+
if not container_image_for_hash(hash):
250247
log.info("Container build incomplete")
251248
return None
252249

@@ -255,7 +252,6 @@ def package_version_for_hash(hash, flavor='default', distro='rhel',
255252
except VersionNotFoundError:
256253
return None
257254

258-
259255
def get_arch(machine_type):
260256
"""
261257
Based on a given machine_type, return its architecture by querying the lock

teuthology/util/containers.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import logging
2+
import re
3+
import requests
4+
5+
log = logging.getLogger(__name__)
6+
7+
# Our container images use a certain base image and flavor by default; those
8+
# values are reflected below. If different values are used, they are appended
9+
# to the image name.
10+
DEFAULT_CONTAINER_BASE = 'centos:9'
11+
DEFAULT_CONTAINER_FLAVOR = 'default'
12+
DEFAULT_CONTAINER_IMAGE='quay.ceph.io/ceph-ci/ceph:{sha1}'
13+
CONTAINER_REGEXP = re.compile(
14+
r"((?P<domain>[a-zA-Z0-9._-]+)/)?((?P<org>[a-zA-Z0-9_-]+)/)?((?P<image>[a-zA-Z0-9_-]+))?(:(?P<tag>[a-zA-Z0-9._-]+))?"
15+
)
16+
17+
18+
def resolve_container_image(image: str):
19+
"""
20+
Given an image locator that is potentially incomplete, construct a qualified version.
21+
22+
':tag' -> 'quay.ceph.io/ceph-ci/ceph:tag'
23+
'image:tag' -> 'quay.ceph.io/ceph-ci/image:tag'
24+
'org/image:tag' -> 'quay.ceph.io/org/image:tag'
25+
'example.com/org/image:tag' -> 'example.com/org/image:tag'
26+
"""
27+
try:
28+
(image_long, tag) = image.split(':')
29+
except ValueError:
30+
raise ValueError(f"Container image spec missing tag: {image}") from None
31+
domain = 'quay.ceph.io'
32+
org = 'ceph-ci'
33+
image = 'ceph'
34+
image_split = image_long.split('/')
35+
assert len(image_split) <= 3
36+
match len(image_split):
37+
case 3:
38+
(domain, org, image) = image_split
39+
case 2:
40+
(org, image) = image_split
41+
case _:
42+
if image_split[0]:
43+
image = image_split[0]
44+
return f"{domain}/{org}/{image}:{tag}"
45+
46+
47+
def container_image_exists(image: str):
48+
"""
49+
Use the Quay API to check for the existence of a container image.
50+
Only tested with Quay registries.
51+
"""
52+
match = re.match(CONTAINER_REGEXP, image)
53+
assert match
54+
obj = match.groupdict()
55+
url = f"https://{obj['domain']}/api/v1/repository/{obj['org']}/{obj['image']}/tag?filter_tag_name=eq:{obj['tag']}"
56+
log.debug(f"Checking for container existence at: {url}")
57+
resp = requests.get(url)
58+
return resp.ok and len(resp.json().get('tags')) >= 1
59+
60+
61+
def container_image_for_hash(hash: str, flavor='default', base_image='centos:9'):
62+
"""
63+
Given a sha1 and optionally a base image and flavor, attempt to return a container image locator.
64+
"""
65+
tag = hash
66+
if base_image != DEFAULT_CONTAINER_BASE:
67+
tag = f"{tag}-{base_image.replace(':', '-')}"
68+
if flavor != DEFAULT_CONTAINER_FLAVOR:
69+
tag = f"{tag}-{flavor}"
70+
image_spec = resolve_container_image(f":{tag}")
71+
if container_image_exists(image_spec):
72+
return image_spec
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import pytest
2+
3+
from unittest.mock import patch
4+
5+
from teuthology.util import containers
6+
7+
@pytest.mark.parametrize(
8+
'input, expected',
9+
[
10+
(':hash', 'quay.ceph.io/ceph-ci/ceph:hash'),
11+
('image:hash', 'quay.ceph.io/ceph-ci/image:hash'),
12+
('org/image:hash', 'quay.ceph.io/org/image:hash'),
13+
('example.com/org/image:hash', 'example.com/org/image:hash'),
14+
('image', ValueError),
15+
('org/image', ValueError),
16+
('domain.net/org/image', ValueError),
17+
]
18+
)
19+
def test_resolve_container_image(input, expected):
20+
if isinstance(expected, str):
21+
assert expected == containers.resolve_container_image(input)
22+
else:
23+
with pytest.raises(expected):
24+
containers.resolve_container_image(input)
25+
26+
@pytest.mark.parametrize(
27+
'image, url',
28+
[
29+
('example.com/org/image:tag', 'https://example.com/api/v1/repository/org/image/tag?filter_tag_name=eq:tag'),
30+
]
31+
)
32+
def test_container_image_exists(image, url):
33+
with patch("teuthology.util.containers.requests.get") as m_get:
34+
containers.container_image_exists(image)
35+
m_get.assert_called_once_with(url)
36+
37+
38+
@pytest.mark.parametrize(
39+
'hash, flavor, base_image, rci_input',
40+
[
41+
('hash', 'flavor', 'base-image', ':hash-base-image-flavor'),
42+
('hash', 'default', 'centos:9', ':hash'),
43+
('hash', 'default', 'rockylinux-10', ':hash-rockylinux-10'),
44+
]
45+
)
46+
def test_container_image_for_hash(hash, flavor, base_image, rci_input):
47+
with patch('teuthology.util.containers.resolve_container_image') as m_rci:
48+
with patch('teuthology.util.containers.container_image_exists'):
49+
containers.container_image_for_hash(hash, flavor, base_image)
50+
m_rci.assert_called_once_with(rci_input)

0 commit comments

Comments
 (0)