-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
VM Tests: Run Image Customizer container. (#21)
Add executing the Image Customizer container in Docker to the basic test. The test suite's Makefile will call the toolkit's Makefile to automatically rebuild the imagecustomizer binary and the container. This avoids the user needing to provide a URL to the container. And it ensures the tests are always running against the code within the checked-out repo.
- Loading branch information
Showing
9 changed files
with
272 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,2 @@ | ||
pytest == 8.3.3 | ||
docker == 7.1.0 | ||
pytest == 8.3.3 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,3 +6,4 @@ black == 24.8.0 | |
flake8 == 7.1.0 | ||
isort == 5.13.2 | ||
mypy == 1.13.0 | ||
types-docker == 7.1.0.20240827 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
# Copyright (c) Microsoft Corporation. | ||
# Licensed under the MIT License. | ||
|
||
import os | ||
import random | ||
import shutil | ||
import string | ||
import tempfile | ||
from pathlib import Path | ||
from typing import Generator | ||
|
||
import docker | ||
import pytest | ||
from docker import DockerClient | ||
|
||
SCRIPT_PATH = Path(__file__).parent | ||
TEST_CONFIGS_DIR = SCRIPT_PATH.joinpath("../../../toolkit/tools/pkg/imagecustomizerlib/testdata") | ||
|
||
|
||
def pytest_addoption(parser: pytest.Parser) -> None: | ||
parser.addoption("--keep-environment", action="store_true", help="Keep the resources created during the test") | ||
parser.addoption("--core-efi-azl2", action="store", help="Path to Azure Linux 2.0 core-efi qcow2 image") | ||
parser.addoption("--image-customizer-container-url", action="store", help="Image Customizer container image URL") | ||
|
||
|
||
@pytest.fixture(scope="session") | ||
def keep_environment(request: pytest.FixtureRequest) -> Generator[bool, None, None]: | ||
flag = request.config.getoption("--keep-environment") | ||
assert isinstance(flag, bool) | ||
yield flag | ||
|
||
|
||
@pytest.fixture(scope="session") | ||
def session_temp_dir(request: pytest.FixtureRequest, keep_environment: bool) -> Generator[Path, None, None]: | ||
build_dir = SCRIPT_PATH.joinpath("build") | ||
os.makedirs(build_dir, exist_ok=True) | ||
|
||
temp_path = tempfile.mkdtemp(prefix="vmtests-", dir=build_dir) | ||
|
||
# Ensure VM can access directory. | ||
os.chmod(temp_path, 0o775) | ||
|
||
yield Path(temp_path) | ||
|
||
if not keep_environment: | ||
shutil.rmtree(temp_path) | ||
|
||
|
||
@pytest.fixture(scope="function") | ||
def test_instance_name(request: pytest.FixtureRequest) -> Generator[str, None, None]: | ||
instance_suffix = "".join(random.choice(string.ascii_uppercase) for _ in range(5)) | ||
yield request.node.name + "-" + instance_suffix | ||
|
||
|
||
# pytest has an in-built fixture called tmp_path. But that uses /tmp, which sits in memory. | ||
# That can be problematic when dealing with image files, which can be quite large. | ||
@pytest.fixture(scope="function") | ||
def test_temp_dir( | ||
request: pytest.FixtureRequest, session_temp_dir: Path, test_instance_name: str, keep_environment: bool | ||
) -> Generator[Path, None, None]: | ||
temp_path = session_temp_dir.joinpath(test_instance_name) | ||
|
||
# Ensure VM can access directory. | ||
temp_path.mkdir(0o775) | ||
|
||
yield Path(temp_path) | ||
|
||
if not keep_environment: | ||
shutil.rmtree(temp_path) | ||
|
||
|
||
@pytest.fixture(scope="session") | ||
def core_efi_azl2(request: pytest.FixtureRequest) -> Generator[Path, None, None]: | ||
image = request.config.getoption("--core-efi-azl2") | ||
if not image: | ||
raise Exception("--core-efi-azl2 is required for test") | ||
yield Path(image) | ||
|
||
|
||
@pytest.fixture(scope="session") | ||
def image_customizer_container_url(request: pytest.FixtureRequest) -> Generator[str, None, None]: | ||
url = request.config.getoption("--image-customizer-container-url") | ||
if not url: | ||
raise Exception("--image-customizer-container-url is required for test") | ||
yield url | ||
|
||
|
||
@pytest.fixture(scope="session") | ||
def docker_client() -> Generator[DockerClient, None, None]: | ||
client = docker.from_env() | ||
yield client | ||
|
||
client.close() # type: ignore |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,28 @@ | ||
# Copyright (c) Microsoft Corporation. | ||
# Licensed under the MIT License. | ||
|
||
from pathlib import Path | ||
|
||
def test_no_change() -> None: | ||
pass | ||
from docker import DockerClient | ||
|
||
from .conftest import TEST_CONFIGS_DIR | ||
from .utils.imagecustomizer import run_image_customizer | ||
|
||
|
||
def test_no_change( | ||
docker_client: DockerClient, | ||
image_customizer_container_url: str, | ||
core_efi_azl2: Path, | ||
test_temp_dir: Path, | ||
) -> None: | ||
config_path = TEST_CONFIGS_DIR.joinpath("nochange-config.yaml") | ||
output_image_path = test_temp_dir.joinpath("image.qcow2") | ||
|
||
run_image_customizer( | ||
docker_client, | ||
image_customizer_container_url, | ||
core_efi_azl2, | ||
config_path, | ||
"qcow2", | ||
output_image_path, | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
# Copyright (c) Microsoft Corporation. | ||
# Licensed under the MIT License. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
# Copyright (c) Microsoft Corporation. | ||
# Licensed under the MIT License. | ||
|
||
import logging | ||
from typing import Any | ||
|
||
import docker | ||
from docker import DockerClient | ||
from docker.models.containers import Container | ||
|
||
|
||
# Can be used with a `with` statement for deleting a Docker container. | ||
class ContainerRemove: | ||
def __init__(self, container: Container): | ||
self.container: Container = container | ||
|
||
def close(self) -> None: | ||
self.container.remove(force=True) | ||
|
||
def __enter__(self) -> "ContainerRemove": | ||
return self | ||
|
||
def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None: | ||
self.close() | ||
|
||
|
||
# Run a container, log its stdout and stderr, and wait for it to complete. | ||
def container_run(docker_client: DockerClient, *args: Any, **kwargs: Any) -> "docker._types.WaitContainerResponse": | ||
with ContainerRemove(docker_client.containers.run(*args, **kwargs)) as container: | ||
return container_log_and_wait(container.container) | ||
|
||
|
||
# Waits for a docker container to exit, while logging stdout and stderr. | ||
def container_log_and_wait(container: Container) -> "docker._types.WaitContainerResponse": | ||
# Log stdout and stderr. | ||
logs = container.logs(stdout=True, stderr=True, stream=True) | ||
for log in logs: | ||
logging.debug(log.decode("utf-8").strip()) | ||
|
||
# Wait for the container to close. | ||
result = container.wait() | ||
|
||
exit_code = result["StatusCode"] | ||
if exit_code != 0: | ||
raise Exception(f"Container failed with {exit_code}") | ||
|
||
return result |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
# Copyright (c) Microsoft Corporation. | ||
# Licensed under the MIT License. | ||
|
||
from pathlib import Path | ||
|
||
from docker import DockerClient | ||
|
||
from .docker_utils import container_run | ||
|
||
|
||
# Run the containerized version of the imagecustomizer tool. | ||
def run_image_customizer( | ||
docker_client: DockerClient, | ||
image_customizer_container_url: str, | ||
base_image_path: Path, | ||
config_path: Path, | ||
output_image_format: str, | ||
output_image_path: Path, | ||
) -> None: | ||
container_base_image_dir = Path("/mic/base_image") | ||
container_config_dir = Path("/mic/config") | ||
container_output_image_dir = Path("/mic/output_image") | ||
container_build_dir = Path("/mic/build") | ||
|
||
base_image_dir = base_image_path.parent.absolute() | ||
config_dir = config_path.parent.absolute() | ||
output_image_dir = output_image_path.parent.absolute() | ||
|
||
container_base_image_path = container_base_image_dir.joinpath(base_image_path.name) | ||
container_config_path = container_config_dir.joinpath(config_path.name) | ||
container_output_image_path = container_output_image_dir.joinpath(output_image_path.name) | ||
|
||
args = [ | ||
"imagecustomizer", | ||
"--image-file", | ||
str(container_base_image_path), | ||
"--config-file", | ||
str(container_config_path), | ||
"--build-dir", | ||
str(container_build_dir), | ||
"--output-image-format", | ||
output_image_format, | ||
"--output-image-file", | ||
str(container_output_image_path), | ||
"--log-level", | ||
"debug", | ||
] | ||
|
||
volumes = [ | ||
f"{base_image_dir}:{container_base_image_dir}:z", | ||
f"{config_dir}:{container_config_dir}:z", | ||
f"{output_image_dir}:{container_output_image_dir}:z", | ||
"/dev:/dev", | ||
] | ||
|
||
container_run(docker_client, image_customizer_container_url, args, detach=True, privileged=True, volumes=volumes) |