diff options
| author | Bruce Ashfield <bruce.ashfield@gmail.com> | 2026-02-06 03:54:31 +0000 |
|---|---|---|
| committer | Bruce Ashfield <bruce.ashfield@gmail.com> | 2026-02-06 03:54:31 +0000 |
| commit | 5aab0f92f1e774305c23802566d75922f65e0862 (patch) | |
| tree | 11193654c643a39e768c267435d562e8e04e8794 | |
| parent | 8c31b451c6f5a9d0bb526ee77f467e5b48846bb4 (diff) | |
| download | meta-virtualization-container-cross-install.tar.gz | |
container-cross-install: add tests and documentation for custom service filescontainer-cross-install
Add pytest tests to verify CONTAINER_SERVICE_FILE varflag support:
TestCustomServiceFileSupport (unit tests, no build required):
- test_bbclass_has_service_file_support
- test_bundle_class_has_service_file_support
- test_service_file_map_syntax
- test_install_custom_service_function
TestCustomServiceFileBoot (boot tests, require built image):
- test_systemd_services_directory_exists
- test_container_services_present
- test_container_service_enabled
- test_custom_service_content
- test_podman_quadlet_directory
Documentation updates:
- docs/container-bundling.md: Add "Custom Service Files" section with
variable format, usage examples for both BUNDLED_CONTAINERS and
container-bundle packages, and example .service/.container files
- tests/README.md: Add test class entries to structure diagram and
"What the Tests Check" table
Signed-off-by: Bruce Ashfield <bruce.ashfield@gmail.com>
| -rw-r--r-- | docs/container-bundling.md | 95 | ||||
| -rw-r--r-- | tests/README.md | 6 | ||||
| -rw-r--r-- | tests/test_container_cross_install.py | 194 |
3 files changed, 294 insertions, 1 deletions
diff --git a/docs/container-bundling.md b/docs/container-bundling.md index 745622b5..f4587a99 100644 --- a/docs/container-bundling.md +++ b/docs/container-bundling.md | |||
| @@ -360,6 +360,101 @@ Containers can be configured to start automatically on boot: | |||
| 360 | - Podman: `/etc/containers/systemd/<name>.container` (Quadlet format) | 360 | - Podman: `/etc/containers/systemd/<name>.container` (Quadlet format) |
| 361 | 361 | ||
| 362 | 362 | ||
| 363 | Custom Service Files | ||
| 364 | -------------------- | ||
| 365 | |||
| 366 | For containers that require specific startup configuration (ports, volumes, | ||
| 367 | capabilities, dependencies), you can provide custom service files instead of | ||
| 368 | using the auto-generated ones. | ||
| 369 | |||
| 370 | ### Variable Format | ||
| 371 | |||
| 372 | Use the `CONTAINER_SERVICE_FILE` varflag to specify custom service files: | ||
| 373 | |||
| 374 | CONTAINER_SERVICE_FILE[container-name] = "${UNPACKDIR}/myservice.service" | ||
| 375 | CONTAINER_SERVICE_FILE[other-container] = "${UNPACKDIR}/other.container" | ||
| 376 | |||
| 377 | ### For BUNDLED_CONTAINERS (in image recipe) | ||
| 378 | |||
| 379 | # host-image.bb or local.conf | ||
| 380 | inherit container-cross-install | ||
| 381 | |||
| 382 | SRC_URI += "\ | ||
| 383 | file://myapp.service \ | ||
| 384 | file://mydb.container \ | ||
| 385 | " | ||
| 386 | |||
| 387 | BUNDLED_CONTAINERS = "\ | ||
| 388 | myapp-container:docker:autostart \ | ||
| 389 | mydb-container:podman:autostart \ | ||
| 390 | " | ||
| 391 | |||
| 392 | # Map containers to custom service files | ||
| 393 | CONTAINER_SERVICE_FILE[myapp-container] = "${UNPACKDIR}/myapp.service" | ||
| 394 | CONTAINER_SERVICE_FILE[mydb-container] = "${UNPACKDIR}/mydb.container" | ||
| 395 | |||
| 396 | ### For container-bundle Packages | ||
| 397 | |||
| 398 | # my-bundle_1.0.bb | ||
| 399 | inherit container-bundle | ||
| 400 | |||
| 401 | SRC_URI = "\ | ||
| 402 | file://myapp.service \ | ||
| 403 | file://mydb.container \ | ||
| 404 | " | ||
| 405 | |||
| 406 | CONTAINER_BUNDLES = "\ | ||
| 407 | myapp-container:autostart \ | ||
| 408 | mydb-container:autostart \ | ||
| 409 | " | ||
| 410 | |||
| 411 | CONTAINER_SERVICE_FILE[myapp-container] = "${UNPACKDIR}/myapp.service" | ||
| 412 | CONTAINER_SERVICE_FILE[mydb-container] = "${UNPACKDIR}/mydb.container" | ||
| 413 | |||
| 414 | ### Docker .service Example | ||
| 415 | |||
| 416 | # myapp.service | ||
| 417 | [Unit] | ||
| 418 | Description=MyApp Container | ||
| 419 | After=docker.service | ||
| 420 | Requires=docker.service | ||
| 421 | |||
| 422 | [Service] | ||
| 423 | Type=simple | ||
| 424 | Restart=unless-stopped | ||
| 425 | RestartSec=5s | ||
| 426 | ExecStartPre=-/usr/bin/docker rm -f myapp | ||
| 427 | ExecStart=/usr/bin/docker run --rm --name myapp \ | ||
| 428 | -p 8080:80 \ | ||
| 429 | -v /data/myapp:/var/lib/myapp:rw \ | ||
| 430 | --cap-add NET_ADMIN \ | ||
| 431 | myapp:latest | ||
| 432 | ExecStop=/usr/bin/docker stop myapp | ||
| 433 | |||
| 434 | [Install] | ||
| 435 | WantedBy=multi-user.target | ||
| 436 | |||
| 437 | ### Podman .container (Quadlet) Example | ||
| 438 | |||
| 439 | # mydb.container | ||
| 440 | [Unit] | ||
| 441 | Description=MyDB Container | ||
| 442 | |||
| 443 | [Container] | ||
| 444 | Image=mydb:latest | ||
| 445 | ContainerName=mydb | ||
| 446 | PublishPort=5432:5432 | ||
| 447 | Volume=/data/db:/var/lib/postgresql/data:Z | ||
| 448 | Environment=POSTGRES_PASSWORD=secret | ||
| 449 | |||
| 450 | [Service] | ||
| 451 | Restart=unless-stopped | ||
| 452 | RestartSec=5s | ||
| 453 | |||
| 454 | [Install] | ||
| 455 | WantedBy=multi-user.target | ||
| 456 | |||
| 457 | |||
| 363 | vdkr and vpdmn - Virtual Container Runtimes | 458 | vdkr and vpdmn - Virtual Container Runtimes |
| 364 | =========================================== | 459 | =========================================== |
| 365 | 460 | ||
diff --git a/tests/README.md b/tests/README.md index 4f5aed28..09bc70ca 100644 --- a/tests/README.md +++ b/tests/README.md | |||
| @@ -162,6 +162,8 @@ BBMULTICONFIG = "vruntime-aarch64 vruntime-x86-64" | |||
| 162 | | `TestVdkrRecipes` | vdkr recipe builds | `bitbake vcontainer-tarball` | | 162 | | `TestVdkrRecipes` | vdkr recipe builds | `bitbake vcontainer-tarball` | |
| 163 | | `TestMulticonfig` | Multiconfig setup | `BBMULTICONFIG` configured | | 163 | | `TestMulticonfig` | Multiconfig setup | `BBMULTICONFIG` configured | |
| 164 | | `TestBundledContainersBoot` | **Boot image and verify containers** | Full image with Docker/Podman | | 164 | | `TestBundledContainersBoot` | **Boot image and verify containers** | Full image with Docker/Podman | |
| 165 | | `TestCustomServiceFileSupport` | CONTAINER_SERVICE_FILE varflag support | None (file check only) | | ||
| 166 | | `TestCustomServiceFileBoot` | Custom service files installed correctly | Full image with autostart containers | | ||
| 165 | 167 | ||
| 166 | ### Boot Tests (TestBundledContainersBoot) | 168 | ### Boot Tests (TestBundledContainersBoot) |
| 167 | 169 | ||
| @@ -440,7 +442,9 @@ tests/ | |||
| 440 | │ ├── TestBundledContainers # end-to-end bundling | 442 | │ ├── TestBundledContainers # end-to-end bundling |
| 441 | │ ├── TestVdkrRecipes # vdkr builds | 443 | │ ├── TestVdkrRecipes # vdkr builds |
| 442 | │ ├── TestMulticonfig # multiconfig setup | 444 | │ ├── TestMulticonfig # multiconfig setup |
| 443 | │ └── TestBundledContainersBoot # boot and verify containers | 445 | │ ├── TestBundledContainersBoot # boot and verify containers |
| 446 | │ ├── TestCustomServiceFileSupport # CONTAINER_SERVICE_FILE varflag support | ||
| 447 | │ └── TestCustomServiceFileBoot # custom service file boot verification | ||
| 444 | ├── test_multiarch_oci.py # Multi-architecture OCI tests | 448 | ├── test_multiarch_oci.py # Multi-architecture OCI tests |
| 445 | │ ├── TestOCIImageIndexDetection # multi-arch OCI detection | 449 | │ ├── TestOCIImageIndexDetection # multi-arch OCI detection |
| 446 | │ ├── TestPlatformSelection # arch selection (aarch64/x86_64) | 450 | │ ├── TestPlatformSelection # arch selection (aarch64/x86_64) |
diff --git a/tests/test_container_cross_install.py b/tests/test_container_cross_install.py index 9a4d23a4..ceb8b874 100644 --- a/tests/test_container_cross_install.py +++ b/tests/test_container_cross_install.py | |||
| @@ -904,3 +904,197 @@ class TestBundledContainersBoot: | |||
| 904 | 904 | ||
| 905 | assert 'CONTAINER_WORKS' in output, \ | 905 | assert 'CONTAINER_WORKS' in output, \ |
| 906 | f"Container {container} failed to run.\nOutput:\n{output}" | 906 | f"Container {container} failed to run.\nOutput:\n{output}" |
| 907 | |||
| 908 | |||
| 909 | # ============================================================================ | ||
| 910 | # Custom Service File Tests | ||
| 911 | # ============================================================================ | ||
| 912 | |||
| 913 | class TestCustomServiceFileSupport: | ||
| 914 | """ | ||
| 915 | Test CONTAINER_SERVICE_FILE varflag support. | ||
| 916 | |||
| 917 | This tests the ability to provide custom systemd service files or | ||
| 918 | Podman Quadlet files instead of auto-generated ones. | ||
| 919 | """ | ||
| 920 | |||
| 921 | def test_bbclass_has_service_file_support(self, meta_virt_dir): | ||
| 922 | """Test that the bbclass includes CONTAINER_SERVICE_FILE support.""" | ||
| 923 | class_file = meta_virt_dir / "classes" / "container-cross-install.bbclass" | ||
| 924 | content = class_file.read_text() | ||
| 925 | |||
| 926 | # Check for the key implementation elements | ||
| 927 | assert "CONTAINER_SERVICE_FILE" in content, \ | ||
| 928 | "CONTAINER_SERVICE_FILE variable not found in bbclass" | ||
| 929 | assert "get_container_service_file_map" in content, \ | ||
| 930 | "get_container_service_file_map function not found" | ||
| 931 | assert "CONTAINER_SERVICE_FILE_MAP" in content, \ | ||
| 932 | "CONTAINER_SERVICE_FILE_MAP variable not found" | ||
| 933 | assert "install_custom_service" in content, \ | ||
| 934 | "install_custom_service function not found" | ||
| 935 | |||
| 936 | def test_bundle_class_has_service_file_support(self, meta_virt_dir): | ||
| 937 | """Test that container-bundle.bbclass includes CONTAINER_SERVICE_FILE support.""" | ||
| 938 | class_file = meta_virt_dir / "classes" / "container-bundle.bbclass" | ||
| 939 | content = class_file.read_text() | ||
| 940 | |||
| 941 | # Check for the key implementation elements | ||
| 942 | assert "CONTAINER_SERVICE_FILE" in content, \ | ||
| 943 | "CONTAINER_SERVICE_FILE variable not found in container-bundle.bbclass" | ||
| 944 | assert "_CONTAINER_SERVICE_FILE_MAP" in content, \ | ||
| 945 | "_CONTAINER_SERVICE_FILE_MAP variable not found" | ||
| 946 | assert "services" in content, \ | ||
| 947 | "services directory handling not found" | ||
| 948 | |||
| 949 | def test_service_file_map_syntax(self, meta_virt_dir): | ||
| 950 | """Test that the service file map function has correct syntax.""" | ||
| 951 | class_file = meta_virt_dir / "classes" / "container-cross-install.bbclass" | ||
| 952 | content = class_file.read_text() | ||
| 953 | |||
| 954 | # Check the function signature and key logic | ||
| 955 | assert "def get_container_service_file_map(d):" in content, \ | ||
| 956 | "get_container_service_file_map function signature not found" | ||
| 957 | assert "getVarFlag('CONTAINER_SERVICE_FILE'" in content, \ | ||
| 958 | "getVarFlag call for CONTAINER_SERVICE_FILE not found" | ||
| 959 | assert 'mappings.append' in content or 'mappings =' in content, \ | ||
| 960 | "Service file mapping logic not found" | ||
| 961 | |||
| 962 | def test_install_custom_service_function(self, meta_virt_dir): | ||
| 963 | """Test that install_custom_service handles both Docker and Podman.""" | ||
| 964 | class_file = meta_virt_dir / "classes" / "container-cross-install.bbclass" | ||
| 965 | content = class_file.read_text() | ||
| 966 | |||
| 967 | # Check the function handles both runtimes | ||
| 968 | assert 'install_custom_service()' in content or 'install_custom_service ' in content, \ | ||
| 969 | "install_custom_service function not found" | ||
| 970 | |||
| 971 | # Docker service installation | ||
| 972 | assert '/lib/systemd/system' in content, \ | ||
| 973 | "Docker service directory path not found" | ||
| 974 | assert 'multi-user.target.wants' in content, \ | ||
| 975 | "Systemd enable symlink path not found" | ||
| 976 | |||
| 977 | # Podman Quadlet installation | ||
| 978 | assert '/etc/containers/systemd' in content, \ | ||
| 979 | "Podman Quadlet directory path not found" | ||
| 980 | |||
| 981 | |||
| 982 | class TestCustomServiceFileBoot: | ||
| 983 | """ | ||
| 984 | Boot tests for custom service files. | ||
| 985 | |||
| 986 | These tests verify that custom service files are properly installed | ||
| 987 | and enabled in the booted system. | ||
| 988 | """ | ||
| 989 | |||
| 990 | @pytest.mark.slow | ||
| 991 | @pytest.mark.boot | ||
| 992 | def test_systemd_services_directory_exists(self, runqemu_session): | ||
| 993 | """Test that systemd service directories exist.""" | ||
| 994 | output = runqemu_session.run_command('ls -la /lib/systemd/system/ | head -5') | ||
| 995 | assert 'systemd' in output or 'total' in output, \ | ||
| 996 | "Systemd system directory not accessible" | ||
| 997 | |||
| 998 | @pytest.mark.slow | ||
| 999 | @pytest.mark.boot | ||
| 1000 | def test_container_services_present(self, runqemu_session, bundled_containers_config): | ||
| 1001 | """Test that container service files are present (custom or generated).""" | ||
| 1002 | docker_containers = bundled_containers_config.get('docker', []) | ||
| 1003 | |||
| 1004 | if not docker_containers: | ||
| 1005 | pytest.skip("No Docker containers configured") | ||
| 1006 | |||
| 1007 | # Check if docker is available | ||
| 1008 | output = runqemu_session.run_command('which docker') | ||
| 1009 | if '/docker' not in output: | ||
| 1010 | pytest.skip("docker not installed in image") | ||
| 1011 | |||
| 1012 | # Check for container service files | ||
| 1013 | output = runqemu_session.run_command('ls /lib/systemd/system/container-*.service 2>/dev/null || echo "NONE"') | ||
| 1014 | |||
| 1015 | if 'NONE' in output: | ||
| 1016 | # No autostart services - check if any containers have autostart | ||
| 1017 | pytest.skip("No container autostart services found (containers may not have autostart enabled)") | ||
| 1018 | |||
| 1019 | # Verify at least one service file exists | ||
| 1020 | assert '.service' in output, \ | ||
| 1021 | f"No container service files found. Output: {output}" | ||
| 1022 | |||
| 1023 | @pytest.mark.slow | ||
| 1024 | @pytest.mark.boot | ||
| 1025 | def test_container_service_enabled(self, runqemu_session, bundled_containers_config): | ||
| 1026 | """Test that container services are enabled (linked in wants directory).""" | ||
| 1027 | docker_containers = bundled_containers_config.get('docker', []) | ||
| 1028 | |||
| 1029 | if not docker_containers: | ||
| 1030 | pytest.skip("No Docker containers configured") | ||
| 1031 | |||
| 1032 | # Check for enabled services in multi-user.target.wants | ||
| 1033 | output = runqemu_session.run_command( | ||
| 1034 | 'ls /etc/systemd/system/multi-user.target.wants/container-*.service 2>/dev/null || echo "NONE"' | ||
| 1035 | ) | ||
| 1036 | |||
| 1037 | if 'NONE' in output: | ||
| 1038 | pytest.skip("No container autostart services enabled") | ||
| 1039 | |||
| 1040 | # Verify services are symlinked | ||
| 1041 | assert '.service' in output, \ | ||
| 1042 | f"No enabled container services found. Output: {output}" | ||
| 1043 | |||
| 1044 | @pytest.mark.slow | ||
| 1045 | @pytest.mark.boot | ||
| 1046 | def test_custom_service_content(self, runqemu_session, bundled_containers_config): | ||
| 1047 | """Test that custom service files have expected content markers.""" | ||
| 1048 | docker_containers = bundled_containers_config.get('docker', []) | ||
| 1049 | |||
| 1050 | if not docker_containers: | ||
| 1051 | pytest.skip("No Docker containers configured") | ||
| 1052 | |||
| 1053 | # Find a container service file | ||
| 1054 | output = runqemu_session.run_command( | ||
| 1055 | 'ls /lib/systemd/system/container-*.service 2>/dev/null | head -1' | ||
| 1056 | ) | ||
| 1057 | |||
| 1058 | if not output or 'container-' not in output: | ||
| 1059 | pytest.skip("No container service files found") | ||
| 1060 | |||
| 1061 | service_file = output.strip().split('\n')[0] | ||
| 1062 | |||
| 1063 | # Read the service file content | ||
| 1064 | content = runqemu_session.run_command(f'cat {service_file}') | ||
| 1065 | |||
| 1066 | # Verify it has expected systemd service structure | ||
| 1067 | assert '[Unit]' in content, f"Service file missing [Unit] section: {service_file}" | ||
| 1068 | assert '[Service]' in content, f"Service file missing [Service] section: {service_file}" | ||
| 1069 | assert '[Install]' in content, f"Service file missing [Install] section: {service_file}" | ||
| 1070 | |||
| 1071 | # Check for docker-related content | ||
| 1072 | assert 'docker' in content.lower(), \ | ||
| 1073 | f"Service file doesn't reference docker: {content}" | ||
| 1074 | |||
| 1075 | @pytest.mark.slow | ||
| 1076 | @pytest.mark.boot | ||
| 1077 | def test_podman_quadlet_directory(self, runqemu_session, bundled_containers_config): | ||
| 1078 | """Test Podman Quadlet directory exists for Podman containers.""" | ||
| 1079 | podman_containers = bundled_containers_config.get('podman', []) | ||
| 1080 | |||
| 1081 | if not podman_containers: | ||
| 1082 | pytest.skip("No Podman containers configured") | ||
| 1083 | |||
| 1084 | # Check if podman is available | ||
| 1085 | output = runqemu_session.run_command('which podman') | ||
| 1086 | if '/podman' not in output: | ||
| 1087 | pytest.skip("podman not installed in image") | ||
| 1088 | |||
| 1089 | # Check for Quadlet directory | ||
| 1090 | output = runqemu_session.run_command('ls -la /etc/containers/systemd/ 2>/dev/null || echo "NONE"') | ||
| 1091 | |||
| 1092 | if 'NONE' in output: | ||
| 1093 | pytest.skip("Quadlet directory not found (containers may not have autostart enabled)") | ||
| 1094 | |||
| 1095 | # Check for .container files | ||
| 1096 | output = runqemu_session.run_command('ls /etc/containers/systemd/*.container 2>/dev/null || echo "NONE"') | ||
| 1097 | |||
| 1098 | if 'NONE' not in output: | ||
| 1099 | assert '.container' in output, \ | ||
| 1100 | f"No Quadlet container files found. Output: {output}" | ||
