Files
test/testcases/cloud_platform/images/test_docker_image_sync.py
Andrew Vaillancourt f5900942e3 Improve Docker registry resolution flexibility
Enhances the DockerConfig design to support more flexible registry
resolution for shared manifests across different environments,
including air-gapped or network-restricted deployments.

Key improvements:
- Refactors manifest_registry_map to support structured entries with
  manifest_registry and override fields.
- Implements clear resolution precedence (per-image, per-manifest,
  global default) via get_effective_source_registry_name().
- Enables declarative overrides so the same manifest files can be
  reused unchanged, while different environments can redirect all
  image sources through configuration alone.
- Allows mixing images with explicit source_registry fields alongside
  manifest-level defaults in the same YAML manifest.
- Adds targeted unit tests for DockerConfig to validate resolution
  behavior and enforce JSON5 schema constraints.

For example, users can share manifests that reference DockerHub, GCR,
or K8s images, and centrally configure all images to be pulled from
an internal mirror registry (e.g., Harbor) without modifying the
manifests themselves.

Change-Id: I9bbd1fe6a23bfa9afada2e4c54399572e1be0ddc
Signed-off-by: Andrew Vaillancourt <andrew.vaillancourt@windriver.com>
2025-07-08 23:19:35 -04:00

190 lines
7.7 KiB
Python

"""
Docker Image Sync Tests Using Manifest-Based Configuration
This module implements foundational tests that verify Docker images listed
in YAML manifest files can be pulled from remote registries (e.g., DockerHub),
tagged, and pushed into the local StarlingX registry (registry.local:9001).
Tests validate both positive and negative scenarios using a config-driven
approach that resolves registries dynamically via ConfigurationManager.
Key Behaviors:
- Validates sync logic from manifest to local registry via SSH.
- Verifies registry resolution order: source_registry, then manifest_registry_map,
then default_source_registry.
- Supports flexible test-driven control over which manifests are synced.
- Logs missing images or partial sync failures for improved debugging.
"""
from pathlib import Path
import yaml
from pytest import FixtureRequest, fail, raises
from config.configuration_manager import ConfigurationManager
from framework.exceptions.keyword_exception import KeywordException
from framework.logging.automation_logger import get_logger
from keywords.cloud_platform.ssh.lab_connection_keywords import LabConnectionKeywords
from keywords.docker.images.docker_images_keywords import DockerImagesKeywords
from keywords.docker.images.docker_sync_images_keywords import DockerSyncImagesKeywords
def run_manifest_sync_test(request: FixtureRequest, manifest_filename: str) -> None:
"""
Executes a manifest-based sync test, pulling Docker images from source registries
and pushing them to the local registry. Verifies that all expected images appear
in the local registry.
Args:
request (FixtureRequest): pytest request object used to register cleanup finalizer.
manifest_filename (str): Path to the manifest file in resources/.
Raises:
AssertionError: If any expected image is missing from the local registry.
"""
ssh_connection = LabConnectionKeywords().get_active_controller_ssh()
docker_config = ConfigurationManager.get_docker_config()
local_registry = docker_config.get_registry("local_registry")
manifest_paths = docker_config.get_image_manifest_files()
manifest_path = next((p for p in manifest_paths if Path(p).name == manifest_filename), None)
if not manifest_path:
raise FileNotFoundError(f"Manifest {manifest_filename} not found in docker config.")
DockerSyncImagesKeywords(ssh_connection).sync_images_from_manifest(manifest_path=manifest_path)
with open(manifest_path, "r") as f:
manifest = yaml.safe_load(f)
docker_image_keywords = DockerImagesKeywords(ssh_connection)
def cleanup():
get_logger().log_info(f"Cleaning up images listed in {manifest_filename}...")
ssh_connection = LabConnectionKeywords().get_active_controller_ssh()
DockerSyncImagesKeywords(ssh_connection).remove_images_from_manifest(manifest_path=manifest_path)
request.addfinalizer(cleanup)
images = docker_image_keywords.list_images()
actual_repos = [img.get_repository() for img in images]
validation_errors = []
for image in manifest["images"]:
name = image["name"]
tag = image["tag"]
expected_ref = f"{local_registry.get_registry_url()}/{name}"
get_logger().log_info(f"Checking local registry for: {expected_ref}:{tag}")
if expected_ref not in actual_repos:
msg = f"[{manifest_filename}] Expected image not found: {expected_ref}"
get_logger().log_warning(msg)
validation_errors.append(msg)
if validation_errors:
raise AssertionError("One or more expected images were not found:\n - " + "\n - ".join(validation_errors))
def test_sync_docker_images_valid_manifest_stx_dockerhub(request):
"""
Validates that all images from a well-formed manifest can be pulled and synced into the local registry.
"""
run_manifest_sync_test(request, "stx-test-images.yaml")
def test_sync_docker_images_invalid_manifest(request):
"""
Negative test: verifies that syncing an invalid manifest raises KeywordException.
This simulates real-world scenarios where image tags are missing or incorrectly referenced.
Very simple brittle string matching is used to verify the exception message.
"""
with raises(KeywordException, match="Image sync failed"):
run_manifest_sync_test(request, "stx-test-images-invalid.yaml")
def test_sync_all_manifests_from_config(request):
"""
Verifies that all manifest files listed in the Docker config can be successfully synced to local registry.
This test ensures that ConfigurationManager.get_docker_config().get_image_manifest_files()
is functional and can drive the sync logic dynamically.
Note: Expected to currently fail if any manifest is invalid or any image fail sync fails.
"""
manifest_paths = ConfigurationManager.get_docker_config().get_image_manifest_files()
get_logger().log_info("Found image manifest paths in config: " + ", ".join(manifest_paths))
for manifest_path in manifest_paths:
manifest_name = Path(manifest_path).name
run_manifest_sync_test(request, manifest_name)
get_logger().log_info(f"All manifests synced successfully. Manifests: {', '.join(manifest_paths)}")
def test_sync_explicit_manifests(request):
"""
Verifies that all manifest files listed in the test case can be successfully synced to local registry.
Note: Expected to currently fail if any manifest is invalid or any image sync fails.
"""
manifest_paths = [
"stx-test-images.yaml",
# "stx-test-images-invalid.yaml"
# Uncomment the above line to include the invalid manifest in the test (and expect failure).
]
get_logger().log_info("Found image manifest paths in config: " + ", ".join(manifest_paths))
for manifest_path in manifest_paths:
manifest_name = Path(manifest_path).name
run_manifest_sync_test(request, manifest_name)
get_logger().log_info(f"All manifests synced successfully. Manifests: {', '.join(manifest_paths)}")
def test_invalid_manifest_logging(request):
"""
Negative test: verifies that syncing an invalid manifest raises KeywordException.
Logs only the image references that actually failed during sync.
"""
manifest_path = "stx-test-images-invalid.yaml"
try:
run_manifest_sync_test(request, manifest_path)
except KeywordException as e:
# Parse individual failure lines from the exception message
failure_lines = [line.strip(" -") for line in str(e).splitlines() if line.strip().startswith("-")]
# Extract just the image reference (everything between 'image ' and ' from ')
failed_images = []
for line in failure_lines:
if "image " in line and " from " in line:
parts = line.split("image ", 1)[-1].split(" from ")[0].strip()
failed_images.append(parts)
else:
failed_images.append(line) # Fallback: use the whole line
formatted_images = "\n\t- " + "\n\t- ".join(failed_images)
get_logger().log_info(f"Negative image sync test passed.\n" f"\tManifest file: {manifest_path}\n" f"\tFailed images:{formatted_images}")
else:
fail("Expected KeywordException was not raised.")
# def test_sync_docker_images_valid_manifest_harbor(request):
# """
# Validates that all images from a well-formed manifest can be pulled and synced into the local registry from a harbor regsitry.
# """
# run_manifest_sync_test(request, "harbor-test-images.yaml")
# def test_sync_docker_images_mixed_registries(request):
# """
# Validates that images from a manifest with mixed registries (DockerHub and Harbor) can be pulled and synced into the local registry.
# """
# run_manifest_sync_test(request, "stx-test-images-mixed-registries.yaml")