]> git-server-git.apps.pok.os.sepia.ceph.com Git - teuthology.git/commitdiff
suite: Check for existence of container images
authorZack Cerza <zack@cerza.org>
Thu, 6 Nov 2025 02:19:01 +0000 (19:19 -0700)
committerZack Cerza <zack@cerza.org>
Thu, 6 Nov 2025 22:58:27 +0000 (15:58 -0700)
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>
teuthology/suite/run.py
teuthology/suite/util.py
teuthology/util/containers.py [new file with mode: 0644]
teuthology/util/test/test_containers.py [new file with mode: 0644]

index 984231dfb1ab92874cc544d78111301beafd4acc..a9ffa212e40f7a90f331aabc28053b9b2283579c 100644 (file)
@@ -25,6 +25,7 @@ from teuthology.suite import util
 from teuthology.suite.merge import config_merge
 from teuthology.suite.build_matrix import build_matrix
 from teuthology.suite.placeholder import substitute_placeholders, dict_templ
+from teuthology.util.containers import container_image_for_hash
 from teuthology.util.time import parse_offset, parse_timestamp, TIMESTAMP_FMT
 
 log = logging.getLogger(__name__)
@@ -549,6 +550,15 @@ class Run(object):
                     # no point in continuing the search
                     if newest:
                         return jobs_missing_packages, []
+                job_tasks = set()
+                for task_dict in parsed_yaml.get('tasks', []):
+                    for key in task_dict.keys():
+                        job_tasks.add(key)
+                if any(['cephadm' in name for name in job_tasks]):
+                    if not container_image_for_hash(sha1):
+                        jobs_missing_packages.append(job)
+                        if newest:
+                            return jobs_missing_packages, []
 
             jobs_to_schedule.append(job)
         return jobs_missing_packages, jobs_to_schedule
index cc884ebf90341ee39784ce38148748eee551355e..251dcc7f7528196018cf15668f90d412d69668fc 100644 (file)
@@ -25,9 +25,6 @@ from teuthology.task.install import get_flavor
 
 log = logging.getLogger(__name__)
 
-CONTAINER_DISTRO = 'centos/9'       # the one to check for build_complete
-CONTAINER_FLAVOR = 'default'
-
 
 def fetch_repos(branch, test_name, dry_run, commit=None):
     """
@@ -245,17 +242,11 @@ def package_version_for_hash(hash, flavor='default', distro='rhel',
         ),
     )
 
-    if (bp.distro == CONTAINER_DISTRO and bp.flavor == CONTAINER_FLAVOR and 
-            not bp.build_complete):
-        log.info("Container build incomplete")
-        return None
-
     try:
         return bp.version
     except VersionNotFoundError:
         return None
 
-
 def get_arch(machine_type):
     """
     Based on a given machine_type, return its architecture by querying the lock
diff --git a/teuthology/util/containers.py b/teuthology/util/containers.py
new file mode 100644 (file)
index 0000000..6614220
--- /dev/null
@@ -0,0 +1,76 @@
+import functools
+import logging
+import re
+import requests
+
+log = logging.getLogger(__name__)
+
+# Our container images use a certain base image and flavor by default; those
+# values are reflected below. If different values are used, they are appended
+# to the image name.
+DEFAULT_CONTAINER_BASE = 'centos:9'
+DEFAULT_CONTAINER_FLAVOR = 'default'
+DEFAULT_CONTAINER_IMAGE='quay.ceph.io/ceph-ci/ceph:{sha1}'
+CONTAINER_REGEXP = re.compile(
+    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._-]+))?"
+)
+
+
+def resolve_container_image(image: str):
+    """
+    Given an image locator that is potentially incomplete, construct a qualified version.
+
+    ':tag' -> 'quay.ceph.io/ceph-ci/ceph:tag'
+    'image:tag' -> 'quay.ceph.io/ceph-ci/image:tag'
+    'org/image:tag' -> 'quay.ceph.io/org/image:tag'
+    'example.com/org/image:tag' -> 'example.com/org/image:tag'
+    """
+    try:
+        (image_long, tag) = image.split(':')
+    except ValueError:
+        raise ValueError(f"Container image spec missing tag: {image}") from None
+    domain = 'quay.ceph.io'
+    org = 'ceph-ci'
+    image = 'ceph'
+    image_split = image_long.split('/')
+    assert len(image_split) <= 3
+    match len(image_split):
+        case 3:
+            (domain, org, image) = image_split
+        case 2:
+            (org, image) = image_split
+        case _:
+            if image_split[0]:
+                image = image_split[0]
+    return f"{domain}/{org}/{image}:{tag}"
+
+
+@functools.lru_cache()
+def container_image_exists(image: str):
+    """
+    Use the Quay API to check for the existence of a container image.
+    Only tested with Quay registries.
+    """
+    match = re.match(CONTAINER_REGEXP, image)
+    assert match
+    obj = match.groupdict()
+    url = f"https://{obj['domain']}/api/v1/repository/{obj['org']}/{obj['image']}/tag?filter_tag_name=eq:{obj['tag']}"
+    log.info(f"Checking for container existence at: {url}")
+    resp = requests.get(url)
+    return resp.ok and len(resp.json().get('tags')) >= 1
+
+
+def container_image_for_hash(hash: str, flavor='default', base_image='centos:9'):
+    """
+    Given a sha1 and optionally a base image and flavor, attempt to return a container image locator.
+    """
+    tag = hash
+    if base_image != DEFAULT_CONTAINER_BASE:
+        tag = f"{tag}-{base_image.replace(':', '-')}"
+    if flavor != DEFAULT_CONTAINER_FLAVOR:
+        tag = f"{tag}-{flavor}"
+    image_spec = resolve_container_image(f":{tag}")
+    if container_image_exists(image_spec):
+        return image_spec
+    else:
+        log.error(f"Container image not found for hash '{hash}'")
diff --git a/teuthology/util/test/test_containers.py b/teuthology/util/test/test_containers.py
new file mode 100644 (file)
index 0000000..8c270f7
--- /dev/null
@@ -0,0 +1,50 @@
+import pytest
+
+from unittest.mock import patch
+
+from teuthology.util import containers
+
+@pytest.mark.parametrize(
+    'input, expected',
+    [
+        (':hash', 'quay.ceph.io/ceph-ci/ceph:hash'),
+        ('image:hash', 'quay.ceph.io/ceph-ci/image:hash'),
+        ('org/image:hash', 'quay.ceph.io/org/image:hash'),
+        ('example.com/org/image:hash', 'example.com/org/image:hash'),
+        ('image', ValueError),
+        ('org/image', ValueError),
+        ('domain.net/org/image', ValueError),
+    ]
+)
+def test_resolve_container_image(input, expected):
+    if isinstance(expected, str):
+        assert expected == containers.resolve_container_image(input)
+    else:
+        with pytest.raises(expected):
+            containers.resolve_container_image(input)
+
+@pytest.mark.parametrize(
+    'image, url',
+    [
+        ('example.com/org/image:tag', 'https://example.com/api/v1/repository/org/image/tag?filter_tag_name=eq:tag'),
+    ]
+)
+def test_container_image_exists(image, url):
+    with patch("teuthology.util.containers.requests.get") as m_get:
+        containers.container_image_exists(image)
+        m_get.assert_called_once_with(url)
+
+
+@pytest.mark.parametrize(
+    'hash, flavor, base_image, rci_input',
+    [
+        ('hash', 'flavor', 'base-image', ':hash-base-image-flavor'),
+        ('hash', 'default', 'centos:9', ':hash'),
+        ('hash', 'default', 'rockylinux-10', ':hash-rockylinux-10'),
+    ]
+)
+def test_container_image_for_hash(hash, flavor, base_image, rci_input):
+    with patch('teuthology.util.containers.resolve_container_image') as m_rci:
+        with patch('teuthology.util.containers.container_image_exists'):
+            containers.container_image_for_hash(hash, flavor, base_image)
+            m_rci.assert_called_once_with(rci_input)