Files
test/testcases/cloud_platform/images/test_docker_image_sync.py
Andrew Vaillancourt 58f64e27f7 Add granular Docker image sync operations
Add support for syncing and removing individual images from manifests
alongside the existing bulk manifest processing.

- Add sync_image_from_manifest() for individual image syncing
- Add remove_image_from_manifest() for selective image removal
- Add image_exists_in_local_registry() for sync confirmation
- Expand test_docker_image_sync tests to illustrate usage
- Add unit tests

This maintains backward compatibility while enabling fine-grained
control for test scenarios requiring individual image management.

Change-Id: Ied9d1595d4c6b406fe0f029893ea84409191d3c2
Signed-off-by: Andrew Vaillancourt <andrew.vaillancourt@windriver.com>
2025-07-10 04:42:04 -04:00

311 lines
14 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 framework.resources.resource_finder import get_stx_resource_path
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")
def test_sync_single_busybox_image(request: FixtureRequest):
"""
Sync a single image (busybox:1.36.1) from the manifest to the local registry,
validate it exists, and clean up afterwards.
This test validates the sync_image_from_manifest() method which allows
selective syncing of individual images rather than entire manifests.
"""
manifest_path = get_stx_resource_path("resources/image_manifests/stx-third-party-test-images.yaml")
ssh_connection = LabConnectionKeywords().get_active_controller_ssh()
docker_sync_keywords = DockerSyncImagesKeywords(ssh_connection)
def cleanup():
get_logger().log_teardown_step("Removing busybox image from local registry")
docker_sync_keywords.remove_image_from_manifest(image_name="busybox", image_tag="1.36.1", manifest_path=manifest_path)
request.addfinalizer(cleanup)
get_logger().log_test_case_step("Syncing busybox image from manifest")
docker_sync_keywords.sync_image_from_manifest(image_name="busybox", image_tag="1.36.1", manifest_path=manifest_path)
get_logger().log_test_case_step("Validating busybox image exists in local registry")
assert docker_sync_keywords.image_exists_in_local_registry(image_name="busybox", image_tag="1.36.1")
def test_sync_third_party_images_to_local_registry(request: FixtureRequest):
"""
Sync required 3rd party Docker images from source registries to local registry.
This test ensures all common sanity test images are preloaded into registry.local.
Uses the stx-third-party-test-images.yaml manifest which contains public images
from DockerHub, k8s.gcr.io, and other public registries.
"""
manifest_path = get_stx_resource_path("resources/image_manifests/stx-third-party-test-images.yaml")
ssh_connection = LabConnectionKeywords().get_active_controller_ssh()
docker_keywords = DockerSyncImagesKeywords(ssh_connection)
def cleanup():
get_logger().log_teardown_step("Removing third-party images from manifest")
docker_keywords.remove_images_from_manifest(manifest_path)
request.addfinalizer(cleanup)
get_logger().log_test_case_step(f"Syncing Docker images from manifest: {manifest_path}")
docker_keywords.sync_images_from_manifest(manifest_path)
get_logger().log_test_case_step("Validating all images exist in local registry")
docker_keywords.manifest_images_exist_in_local_registry(manifest_path)
get_logger().log_info("Successfully synced all 3rd party test images")
def test_remove_third_party_images_from_local_registry():
"""
Remove third-party Docker images from registry.local that were synced from manifest.
This test ensures cleanup of common sanity/networking images after test execution.
Can be run independently or as part of test cleanup verification.
"""
manifest_path = get_stx_resource_path("resources/image_manifests/stx-third-party-test-images.yaml")
get_logger().log_test_case_step("Connecting to active controller")
ssh_connection = LabConnectionKeywords().get_active_controller_ssh()
get_logger().log_test_case_step(f"Removing Docker images listed in: {manifest_path}")
docker_keywords = DockerSyncImagesKeywords(ssh_connection=ssh_connection)
docker_keywords.remove_images_from_manifest(manifest_path)
get_logger().log_info("Successfully removed all third-party test images")
def test_single_image_sync_and_removal_workflow(request: FixtureRequest):
"""
Test the complete workflow of syncing and removing a single image.
This test validates:
1. Single image sync from manifest
2. Image validation in local registry
3. Single image removal from manifest
4. Cleanup verification
"""
manifest_path = get_stx_resource_path("resources/image_manifests/stx-third-party-test-images.yaml")
ssh_connection = LabConnectionKeywords().get_active_controller_ssh()
docker_sync_keywords = DockerSyncImagesKeywords(ssh_connection)
# Test image: calico/ctl:v3.27.0 (from the third-party manifest)
test_image_name = "calico/ctl"
test_image_tag = "v3.27.0"
def cleanup():
get_logger().log_teardown_step(f"Final cleanup: removing {test_image_name}:{test_image_tag}")
try:
docker_sync_keywords.remove_image_from_manifest(image_name=test_image_name, image_tag=test_image_tag, manifest_path=manifest_path)
except Exception as e:
get_logger().log_warning(f"Cleanup failed (expected if test passed): {e}")
request.addfinalizer(cleanup)
# Step 1: Sync the single image
get_logger().log_test_case_step(f"Syncing {test_image_name}:{test_image_tag} from manifest")
docker_sync_keywords.sync_image_from_manifest(image_name=test_image_name, image_tag=test_image_tag, manifest_path=manifest_path)
# Step 2: Validate it exists in local registry
get_logger().log_test_case_step(f"Validating {test_image_name}:{test_image_tag} exists in local registry")
assert docker_sync_keywords.image_exists_in_local_registry(image_name=test_image_name, image_tag=test_image_tag), f"Image {test_image_name}:{test_image_tag} should exist in local registry after sync"
# Step 3: Remove the single image
get_logger().log_test_case_step(f"Removing {test_image_name}:{test_image_tag} from local registry")
docker_sync_keywords.remove_image_from_manifest(image_name=test_image_name, image_tag=test_image_tag, manifest_path=manifest_path)
# Step 4: Validate it no longer exists (optional verification)
get_logger().log_test_case_step(f"Verifying {test_image_name}:{test_image_tag} was removed")
assert not docker_sync_keywords.image_exists_in_local_registry(image_name=test_image_name, image_tag=test_image_tag), f"Image {test_image_name}:{test_image_tag} should not exist in local registry after removal"
get_logger().log_info(f"Successfully completed single image sync/removal workflow for {test_image_name}:{test_image_tag}")