Skip to content

Commit 0a6690b

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. This check will only affect jobs which use the cephadm task. Signed-off-by: Zack Cerza <zack@cerza.org>
1 parent b056b94 commit 0a6690b

File tree

4 files changed

+135
-9
lines changed

4 files changed

+135
-9
lines changed

teuthology/suite/run.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -549,6 +549,15 @@ def collect_jobs(self, arch, configs, newest=False, limit=0):
549549
# no point in continuing the search
550550
if newest:
551551
return jobs_missing_packages, []
552+
job_tasks = set()
553+
for task_dict in parsed_yaml.get('tasks', []):
554+
for key in task_dict.keys():
555+
job_tasks.add(key)
556+
if any(['cephadm' in name for name in job_tasks]):
557+
if not util.container_image_for_hash(sha1):
558+
jobs_missing_packages.append(job)
559+
if newest:
560+
return jobs_missing_packages, []
552561

553562
jobs_to_schedule.append(job)
554563
return jobs_missing_packages, jobs_to_schedule

teuthology/suite/util.py

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,6 @@
2525

2626
log = logging.getLogger(__name__)
2727

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

3229
def fetch_repos(branch, test_name, dry_run, commit=None):
3330
"""
@@ -245,17 +242,11 @@ def package_version_for_hash(hash, flavor='default', distro='rhel',
245242
),
246243
)
247244

248-
if (bp.distro == CONTAINER_DISTRO and bp.flavor == CONTAINER_FLAVOR and
249-
not bp.build_complete):
250-
log.info("Container build incomplete")
251-
return None
252-
253245
try:
254246
return bp.version
255247
except VersionNotFoundError:
256248
return None
257249

258-
259250
def get_arch(machine_type):
260251
"""
261252
Based on a given machine_type, return its architecture by querying the lock

teuthology/util/containers.py

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