Files
test/testcases/cloud_platform/images/test_docker_image_sync.py
Andrew Vaillancourt 0796e5062a PoC: manifest-driven image sync for test images
This patch introduces a foundational test and keyword framework
for syncing Docker images from external registries (e.g., DockerHub)
into the local StarlingX registry at registry.local:9001. Sync
behavior is driven by YAML manifests and resolved using the
JSON5-based ConfigurationManager system already used throughout
starlingx/test.

Key Features:
- Supports multiple image manifests and logical registry mappings
  defined in config/docker/files/default.json5.
- Registry resolution follows:
  1. source_registry field (per image in manifest)
  2. manifest_registry_map (per manifest)
  3. default_source_registry (global fallback)
- Test coverage verifies that registry resolution honors override
  order, ensuring images are pulled from the correct source based
  on per-image fields and per-manifest mappings, using
  default_source_registry only when no override is provided.

Forward Compatibility:
- The config and manifest format is designed to support future
  extensions such as digest pinning, curated test image sets,
  or internal registry mirroring.
- Test images currently reference stable tags from
  https://hub.docker.com/u/starlingx and will later be moved to
  a dedicated test image repo.

This patch lays the foundation for managing image dependencies
through versioned manifests rather than bundling image binaries
in the repository.

Change-Id: Ib0bdf8ade444f079b141baed680eb1e71ed7cd0a
Signed-off-by: Andrew Vaillancourt <andrew.vaillancourt@windriver.com>
2025-06-12 02:52:12 -04:00

234 lines
9.6 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():
"""
Cleans up Docker images listed in the manifest from the local system.
For each image, up to three tag formats are removed from the local Docker cache:
1. source_registry/image:tag (skipped if source is docker.io; see note below)
2. local_registry/image:tag (e.g., image pushed to registry.local)
3. image:tag (default short form used by Docker)
Purpose:
- Ensures complete removal regardless of how the image was tagged during pull/push operations.
- Supports idempotent cleanup, avoiding reliance on a single canonical tag.
- Handles cases where Docker implicitly normalizes or aliases tag references.
Notes:
- Full `docker.io/...` references are skipped during cleanup, as Docker stores these as `image:tag`.
"""
get_logger().log_info(f"Cleaning up images listed in {manifest_filename}...")
ssh_connection = LabConnectionKeywords().get_active_controller_ssh()
docker_image_keywords_cleanup = DockerImagesKeywords(ssh_connection)
for image in manifest.get("images", []):
name = image["name"]
tag = image["tag"]
source_registry_name = docker_config.get_effective_source_registry_name(image, manifest_filename)
if not source_registry_name:
get_logger().log_debug(f"Skipping cleanup for image {name}:{tag} (no source registry resolved)")
continue
source_registry = docker_config.get_registry(source_registry_name)
source_url = source_registry.get_registry_url()
# Always try to remove these two references
refs = [
f"{local_registry.get_registry_url()}/{name}:{tag}",
f"{name}:{tag}",
]
# Optionally add full source registry tag if not DockerHub
if "docker.io" not in source_url:
refs.insert(0, f"{source_url}/{name}:{tag}")
else:
get_logger().log_debug(f"Skipping full docker.io-prefixed tag for {source_url}/{name}:{tag}")
for ref in refs:
docker_image_keywords_cleanup.remove_image(ref)
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")