diff options
| author | Bruce Ashfield <bruce.ashfield@gmail.com> | 2026-02-06 03:54:16 +0000 |
|---|---|---|
| committer | Bruce Ashfield <bruce.ashfield@gmail.com> | 2026-02-09 03:34:12 +0000 |
| commit | cd5081a5e9ff1c6f5eb74ab90326d602142248f9 (patch) | |
| tree | 942b4954cb73e217d5c914d381d262b231ee8ab8 /classes | |
| parent | b4ad3f9eb2f022b6f69b2e78dbca80974d5bf84a (diff) | |
| download | meta-virtualization-cd5081a5e9ff1c6f5eb74ab90326d602142248f9.tar.gz | |
container-cross-install: add CONTAINER_SERVICE_FILE support
Add support for custom systemd service files (Docker) or Quadlet
container files (Podman) instead of auto-generated ones for container
autostart.
For containers requiring specific startup configuration (ports, volumes,
capabilities, dependencies), users can now provide custom service files
using the CONTAINER_SERVICE_FILE varflag:
CONTAINER_SERVICE_FILE[container-name] = "${UNPACKDIR}/myservice.service"
For BUNDLED_CONTAINERS in image recipes:
SRC_URI += "file://myapp.service"
BUNDLED_CONTAINERS = "myapp-container:docker:autostart"
CONTAINER_SERVICE_FILE[myapp-container] = "${UNPACKDIR}/myapp.service"
For container-bundle packages:
SRC_URI = "file://myapp.service"
CONTAINER_BUNDLES = "myapp-container:autostart"
CONTAINER_SERVICE_FILE[myapp-container] = "${UNPACKDIR}/myapp.service"
Implementation:
- container-cross-install.bbclass: Add get_container_service_file_map()
to build varflag map, install_custom_service() for BUNDLED_CONTAINERS,
and install_custom_service_from_bundle() for bundle packages
- container-bundle.bbclass: Install custom service files to
${datadir}/container-bundles/${runtime}/services/
Signed-off-by: Bruce Ashfield <bruce.ashfield@gmail.com>
Diffstat (limited to 'classes')
| -rw-r--r-- | classes/container-bundle.bbclass | 80 | ||||
| -rw-r--r-- | classes/container-cross-install.bbclass | 157 |
2 files changed, 236 insertions, 1 deletions
diff --git a/classes/container-bundle.bbclass b/classes/container-bundle.bbclass index 3d3f3a16..3c8ad030 100644 --- a/classes/container-bundle.bbclass +++ b/classes/container-bundle.bbclass | |||
| @@ -141,6 +141,30 @@ | |||
| 141 | # The runtime subdirectory (docker/ vs podman/) tells container-cross-install | 141 | # The runtime subdirectory (docker/ vs podman/) tells container-cross-install |
| 142 | # which vrunner runtime to use for import. | 142 | # which vrunner runtime to use for import. |
| 143 | # | 143 | # |
| 144 | # =========================================================================== | ||
| 145 | # Custom Service Files (CONTAINER_SERVICE_FILE) | ||
| 146 | # =========================================================================== | ||
| 147 | # | ||
| 148 | # For containers requiring specific startup configuration, provide custom | ||
| 149 | # service files instead of auto-generated ones: | ||
| 150 | # | ||
| 151 | # SRC_URI = "file://myapp.service file://mydb.container" | ||
| 152 | # | ||
| 153 | # CONTAINER_BUNDLES = "\ | ||
| 154 | # myapp-container:autostart \ | ||
| 155 | # mydb-container:autostart \ | ||
| 156 | # " | ||
| 157 | # | ||
| 158 | # CONTAINER_SERVICE_FILE[myapp-container] = "${UNPACKDIR}/myapp.service" | ||
| 159 | # CONTAINER_SERVICE_FILE[mydb-container] = "${UNPACKDIR}/mydb.container" | ||
| 160 | # | ||
| 161 | # Custom files are installed to ${datadir}/container-bundles/${RUNTIME}/services/ | ||
| 162 | # and used by container-cross-install instead of generating default services. | ||
| 163 | # | ||
| 164 | # For Docker, provide a .service file; for Podman, provide a .container Quadlet. | ||
| 165 | # | ||
| 166 | # See docs/container-bundling.md for detailed examples. | ||
| 167 | # | ||
| 144 | # See also: container-cross-install.bbclass | 168 | # See also: container-cross-install.bbclass |
| 145 | 169 | ||
| 146 | CONTAINER_BUNDLES ?= "" | 170 | CONTAINER_BUNDLES ?= "" |
| @@ -247,6 +271,29 @@ python __anonymous() { | |||
| 247 | d.setVar('_REMOTE_CONTAINERS', ' '.join(remote_urls)) | 271 | d.setVar('_REMOTE_CONTAINERS', ' '.join(remote_urls)) |
| 248 | d.setVar('_PROCESSED_BUNDLES', ' '.join(processed_bundles)) | 272 | d.setVar('_PROCESSED_BUNDLES', ' '.join(processed_bundles)) |
| 249 | d.setVar('_BUNDLE_RUNTIME', runtime) | 273 | d.setVar('_BUNDLE_RUNTIME', runtime) |
| 274 | |||
| 275 | # Build service file map for custom service files | ||
| 276 | # Format: container1=/path/to/file1;container2=/path/to/file2 | ||
| 277 | service_mappings = [] | ||
| 278 | for bundle in bundles: | ||
| 279 | # Extract container name (handle both local and remote formats) | ||
| 280 | if is_remote_container(bundle): | ||
| 281 | if bundle.endswith(':autostart') or bundle.endswith(':always') or \ | ||
| 282 | bundle.endswith(':unless-stopped') or bundle.endswith(':on-failure') or \ | ||
| 283 | bundle.endswith(':no'): | ||
| 284 | last_colon = bundle.rfind(':') | ||
| 285 | source = bundle[:last_colon] | ||
| 286 | else: | ||
| 287 | source = bundle | ||
| 288 | else: | ||
| 289 | parts = bundle.split(':') | ||
| 290 | source = parts[0] | ||
| 291 | |||
| 292 | custom_file = d.getVarFlag('CONTAINER_SERVICE_FILE', source) | ||
| 293 | if custom_file: | ||
| 294 | service_mappings.append(f"{source}={custom_file}") | ||
| 295 | |||
| 296 | d.setVar('_CONTAINER_SERVICE_FILE_MAP', ';'.join(service_mappings)) | ||
| 250 | } | 297 | } |
| 251 | 298 | ||
| 252 | # S must be a real directory | 299 | # S must be a real directory |
| @@ -402,6 +449,39 @@ do_install() { | |||
| 402 | install -m 0644 ${B}/bundle-metadata.txt \ | 449 | install -m 0644 ${B}/bundle-metadata.txt \ |
| 403 | ${D}${datadir}/container-bundles/${PN}.meta | 450 | ${D}${datadir}/container-bundles/${PN}.meta |
| 404 | fi | 451 | fi |
| 452 | |||
| 453 | # Install custom service files from CONTAINER_SERVICE_FILE varflags | ||
| 454 | # Format: container1=/path/to/file1;container2=/path/to/file2 | ||
| 455 | if [ -n "${_CONTAINER_SERVICE_FILE_MAP}" ]; then | ||
| 456 | install -d ${D}${datadir}/container-bundles/${RUNTIME}/services | ||
| 457 | echo "${_CONTAINER_SERVICE_FILE_MAP}" | tr ';' '\n' | while IFS='=' read -r container_name service_file; do | ||
| 458 | [ -z "$container_name" ] && continue | ||
| 459 | [ -z "$service_file" ] && continue | ||
| 460 | |||
| 461 | if [ ! -f "$service_file" ]; then | ||
| 462 | bbwarn "Custom service file not found: $service_file (for container $container_name)" | ||
| 463 | continue | ||
| 464 | fi | ||
| 465 | |||
| 466 | # Sanitize container name for filename (replace / and : with _) | ||
| 467 | local sanitized_name=$(echo "$container_name" | sed 's|[/:]|_|g') | ||
| 468 | |||
| 469 | # Determine file extension based on runtime and source file | ||
| 470 | local dest_file | ||
| 471 | if [ "${RUNTIME}" = "docker" ]; then | ||
| 472 | dest_file="${sanitized_name}.service" | ||
| 473 | elif [ "${RUNTIME}" = "podman" ]; then | ||
| 474 | dest_file="${sanitized_name}.container" | ||
| 475 | else | ||
| 476 | # Keep original extension | ||
| 477 | dest_file="${sanitized_name}.$(echo "$service_file" | sed 's/.*\.//')" | ||
| 478 | fi | ||
| 479 | |||
| 480 | bbnote "Installing custom service file: $service_file -> services/${dest_file}" | ||
| 481 | install -m 0644 "$service_file" \ | ||
| 482 | ${D}${datadir}/container-bundles/${RUNTIME}/services/${dest_file} | ||
| 483 | done | ||
| 484 | fi | ||
| 405 | } | 485 | } |
| 406 | 486 | ||
| 407 | FILES:${PN} = "${datadir}/container-bundles" | 487 | FILES:${PN} = "${datadir}/container-bundles" |
diff --git a/classes/container-cross-install.bbclass b/classes/container-cross-install.bbclass index d18c6436..bb2547e0 100644 --- a/classes/container-cross-install.bbclass +++ b/classes/container-cross-install.bbclass | |||
| @@ -98,6 +98,27 @@ | |||
| 98 | # The runtime is determined by the subdirectory (docker/ vs podman/), | 98 | # The runtime is determined by the subdirectory (docker/ vs podman/), |
| 99 | # which is set by container-bundle.bbclass based on CONTAINER_BUNDLE_RUNTIME. | 99 | # which is set by container-bundle.bbclass based on CONTAINER_BUNDLE_RUNTIME. |
| 100 | # | 100 | # |
| 101 | # =========================================================================== | ||
| 102 | # Custom Service Files (CONTAINER_SERVICE_FILE) | ||
| 103 | # =========================================================================== | ||
| 104 | # | ||
| 105 | # For containers requiring specific startup configuration (ports, volumes, | ||
| 106 | # capabilities, dependencies), provide custom service files instead of | ||
| 107 | # auto-generated ones using the CONTAINER_SERVICE_FILE varflag: | ||
| 108 | # | ||
| 109 | # CONTAINER_SERVICE_FILE[container-name] = "${UNPACKDIR}/myservice.service" | ||
| 110 | # CONTAINER_SERVICE_FILE[other-container] = "${UNPACKDIR}/other.container" | ||
| 111 | # | ||
| 112 | # Usage in image recipe: | ||
| 113 | # SRC_URI += "file://myapp.service" | ||
| 114 | # BUNDLED_CONTAINERS = "myapp-container:docker:autostart" | ||
| 115 | # CONTAINER_SERVICE_FILE[myapp-container] = "${UNPACKDIR}/myapp.service" | ||
| 116 | # | ||
| 117 | # The custom file replaces the auto-generated service. For Docker, provide | ||
| 118 | # a .service file; for Podman, provide a .container Quadlet file. | ||
| 119 | # | ||
| 120 | # See docs/container-bundling.md for detailed examples. | ||
| 121 | # | ||
| 101 | # See also: container-bundle.bbclass | 122 | # See also: container-bundle.bbclass |
| 102 | 123 | ||
| 103 | # Inherit shared functions for multiconfig/machine/arch mapping | 124 | # Inherit shared functions for multiconfig/machine/arch mapping |
| @@ -164,6 +185,26 @@ python __anonymous() { | |||
| 164 | d.appendVarFlag('do_rootfs', 'depends', deps) | 185 | d.appendVarFlag('do_rootfs', 'depends', deps) |
| 165 | } | 186 | } |
| 166 | 187 | ||
| 188 | # Build CONTAINER_SERVICE_FILE_MAP from varflags for shell access | ||
| 189 | # Format: container1=/path/to/file1;container2=/path/to/file2 | ||
| 190 | def get_container_service_file_map(d): | ||
| 191 | """Build a semicolon-separated map of container names to custom service files""" | ||
| 192 | bundled = (d.getVar('BUNDLED_CONTAINERS') or "").split() | ||
| 193 | if not bundled: | ||
| 194 | return "" | ||
| 195 | |||
| 196 | mappings = [] | ||
| 197 | for entry in bundled: | ||
| 198 | parts = entry.split(':') | ||
| 199 | container_name = parts[0] | ||
| 200 | custom_file = d.getVarFlag('CONTAINER_SERVICE_FILE', container_name) | ||
| 201 | if custom_file: | ||
| 202 | mappings.append(f"{container_name}={custom_file}") | ||
| 203 | |||
| 204 | return ";".join(mappings) | ||
| 205 | |||
| 206 | CONTAINER_SERVICE_FILE_MAP = "${@get_container_service_file_map(d)}" | ||
| 207 | |||
| 167 | # Path to vrunner.sh from vcontainer-native | 208 | # Path to vrunner.sh from vcontainer-native |
| 168 | VRUNNER_PATH = "${STAGING_BINDIR_NATIVE}/vrunner.sh" | 209 | VRUNNER_PATH = "${STAGING_BINDIR_NATIVE}/vrunner.sh" |
| 169 | 210 | ||
| @@ -419,7 +460,21 @@ merge_installed_bundles() { | |||
| 419 | 460 | ||
| 420 | bbnote "Creating autostart service for $source ($runtime_type, restart=$restart_policy)" | 461 | bbnote "Creating autostart service for $source ($runtime_type, restart=$restart_policy)" |
| 421 | 462 | ||
| 463 | # Check for custom service file in bundle's services directory | ||
| 464 | # Custom files are stored as: services/<source-sanitized>.(service|container) | ||
| 465 | local source_sanitized=$(echo "$source" | sed 's|[/:]|_|g') | ||
| 466 | local custom_service_file="" | ||
| 467 | |||
| 422 | if [ "$runtime_type" = "docker" ]; then | 468 | if [ "$runtime_type" = "docker" ]; then |
| 469 | custom_service_file="${BUNDLES_DIR}/${runtime_type}/services/${source_sanitized}.service" | ||
| 470 | elif [ "$runtime_type" = "podman" ]; then | ||
| 471 | custom_service_file="${BUNDLES_DIR}/${runtime_type}/services/${source_sanitized}.container" | ||
| 472 | fi | ||
| 473 | |||
| 474 | if [ -n "$custom_service_file" ] && [ -f "$custom_service_file" ]; then | ||
| 475 | bbnote "Using custom service file from bundle: $custom_service_file" | ||
| 476 | install_custom_service_from_bundle "$source" "$service_name" "$runtime_type" "$custom_service_file" | ||
| 477 | elif [ "$runtime_type" = "docker" ]; then | ||
| 423 | generate_docker_service_from_bundle "$service_name" "$image_name" "$image_tag" "$restart_policy" | 478 | generate_docker_service_from_bundle "$service_name" "$image_name" "$image_tag" "$restart_policy" |
| 424 | elif [ "$runtime_type" = "podman" ]; then | 479 | elif [ "$runtime_type" = "podman" ]; then |
| 425 | generate_podman_service_from_bundle "$service_name" "$image_name" "$image_tag" "$restart_policy" | 480 | generate_podman_service_from_bundle "$service_name" "$image_name" "$image_tag" "$restart_policy" |
| @@ -434,6 +489,51 @@ merge_installed_bundles() { | |||
| 434 | return 0 | 489 | return 0 |
| 435 | } | 490 | } |
| 436 | 491 | ||
| 492 | # Install a custom service file from a bundle package | ||
| 493 | # Args: source service_name runtime_type custom_file | ||
| 494 | install_custom_service_from_bundle() { | ||
| 495 | local source="$1" | ||
| 496 | local service_name="$2" | ||
| 497 | local runtime_type="$3" | ||
| 498 | local custom_file="$4" | ||
| 499 | |||
| 500 | if [ ! -f "$custom_file" ]; then | ||
| 501 | bbwarn "Custom service file not found: $custom_file (for container $source)" | ||
| 502 | return 1 | ||
| 503 | fi | ||
| 504 | |||
| 505 | if [ "$runtime_type" = "docker" ]; then | ||
| 506 | # Docker: Install as systemd service | ||
| 507 | local service_dir="${IMAGE_ROOTFS}/lib/systemd/system" | ||
| 508 | local service_file="${service_dir}/${service_name}.service" | ||
| 509 | |||
| 510 | mkdir -p "$service_dir" | ||
| 511 | install -m 0644 "$custom_file" "$service_file" | ||
| 512 | |||
| 513 | # Enable the service via symlink | ||
| 514 | local wants_dir="${IMAGE_ROOTFS}/etc/systemd/system/multi-user.target.wants" | ||
| 515 | mkdir -p "$wants_dir" | ||
| 516 | ln -sf "/lib/systemd/system/${service_name}.service" "${wants_dir}/${service_name}.service" | ||
| 517 | |||
| 518 | bbnote "Installed custom service from bundle: $custom_file -> ${service_name}.service" | ||
| 519 | |||
| 520 | elif [ "$runtime_type" = "podman" ]; then | ||
| 521 | # Podman: Install as Quadlet container file | ||
| 522 | local quadlet_dir="${IMAGE_ROOTFS}/etc/containers/systemd" | ||
| 523 | local container_file="${quadlet_dir}/${service_name}.container" | ||
| 524 | |||
| 525 | mkdir -p "$quadlet_dir" | ||
| 526 | install -m 0644 "$custom_file" "$container_file" | ||
| 527 | |||
| 528 | bbnote "Installed custom Quadlet file from bundle: $custom_file -> ${service_name}.container" | ||
| 529 | else | ||
| 530 | bbwarn "Unknown runtime '$runtime_type' for custom service file from bundle, skipping" | ||
| 531 | return 1 | ||
| 532 | fi | ||
| 533 | |||
| 534 | return 0 | ||
| 535 | } | ||
| 536 | |||
| 437 | # Generate Docker systemd service (for bundle packages) | 537 | # Generate Docker systemd service (for bundle packages) |
| 438 | generate_docker_service_from_bundle() { | 538 | generate_docker_service_from_bundle() { |
| 439 | local service_name="$1" | 539 | local service_name="$1" |
| @@ -648,6 +748,47 @@ EOF | |||
| 648 | bbnote "Created Quadlet file ${service_name}.container for Podman container ${image_name}:${image_tag}" | 748 | bbnote "Created Quadlet file ${service_name}.container for Podman container ${image_name}:${image_tag}" |
| 649 | } | 749 | } |
| 650 | 750 | ||
| 751 | # Install a custom service file provided by the user | ||
| 752 | # Args: container_name service_name runtime_type custom_file | ||
| 753 | install_custom_service() { | ||
| 754 | local container_name="$1" | ||
| 755 | local service_name="$2" | ||
| 756 | local runtime_type="$3" | ||
| 757 | local custom_file="$4" | ||
| 758 | |||
| 759 | if [ ! -f "$custom_file" ]; then | ||
| 760 | bbfatal "Custom service file not found: $custom_file (for container $container_name)" | ||
| 761 | fi | ||
| 762 | |||
| 763 | if [ "$runtime_type" = "docker" ]; then | ||
| 764 | # Docker: Install as systemd service | ||
| 765 | local service_dir="${IMAGE_ROOTFS}/lib/systemd/system" | ||
| 766 | local service_file="${service_dir}/${service_name}.service" | ||
| 767 | |||
| 768 | mkdir -p "$service_dir" | ||
| 769 | install -m 0644 "$custom_file" "$service_file" | ||
| 770 | |||
| 771 | # Enable the service via symlink | ||
| 772 | local wants_dir="${IMAGE_ROOTFS}/etc/systemd/system/multi-user.target.wants" | ||
| 773 | mkdir -p "$wants_dir" | ||
| 774 | ln -sf "/lib/systemd/system/${service_name}.service" "${wants_dir}/${service_name}.service" | ||
| 775 | |||
| 776 | bbnote "Installed custom service: $custom_file -> ${service_name}.service" | ||
| 777 | |||
| 778 | elif [ "$runtime_type" = "podman" ]; then | ||
| 779 | # Podman: Install as Quadlet container file | ||
| 780 | local quadlet_dir="${IMAGE_ROOTFS}/etc/containers/systemd" | ||
| 781 | local container_file="${quadlet_dir}/${service_name}.container" | ||
| 782 | |||
| 783 | mkdir -p "$quadlet_dir" | ||
| 784 | install -m 0644 "$custom_file" "$container_file" | ||
| 785 | |||
| 786 | bbnote "Installed custom Quadlet file: $custom_file -> ${service_name}.container" | ||
| 787 | else | ||
| 788 | bbwarn "Unknown runtime '$runtime_type' for custom service file, skipping" | ||
| 789 | fi | ||
| 790 | } | ||
| 791 | |||
| 651 | # Install autostart services for containers with autostart policy | 792 | # Install autostart services for containers with autostart policy |
| 652 | install_autostart_services() { | 793 | install_autostart_services() { |
| 653 | bbnote "Processing container autostart services..." | 794 | bbnote "Processing container autostart services..." |
| @@ -697,7 +838,21 @@ EOF | |||
| 697 | 838 | ||
| 698 | bbnote "Creating autostart service for $container_name ($runtime_type, restart=$restart_policy)" | 839 | bbnote "Creating autostart service for $container_name ($runtime_type, restart=$restart_policy)" |
| 699 | 840 | ||
| 700 | if [ "$runtime_type" = "docker" ]; then | 841 | # Check for custom service file via CONTAINER_SERVICE_FILE varflag |
| 842 | # This is evaluated at rootfs time, so we use a Python helper | ||
| 843 | local custom_service_file="${CONTAINER_SERVICE_FILE_MAP}" | ||
| 844 | local custom_file="" | ||
| 845 | |||
| 846 | # Parse the map to find this container's custom file | ||
| 847 | # Format: container1=/path/to/file1;container2=/path/to/file2 | ||
| 848 | if [ -n "$custom_service_file" ]; then | ||
| 849 | custom_file=$(echo "$custom_service_file" | tr ';' '\n' | grep "^${container_name}=" | cut -d= -f2-) | ||
| 850 | fi | ||
| 851 | |||
| 852 | if [ -n "$custom_file" ] && [ -f "$custom_file" ]; then | ||
| 853 | bbnote "Using custom service file for $container_name: $custom_file" | ||
| 854 | install_custom_service "$container_name" "$service_name" "$runtime_type" "$custom_file" | ||
| 855 | elif [ "$runtime_type" = "docker" ]; then | ||
| 701 | generate_docker_service "$service_name" "${CONTAINER_IMAGE_NAME}" "${CONTAINER_IMAGE_TAG}" "$restart_policy" | 856 | generate_docker_service "$service_name" "${CONTAINER_IMAGE_NAME}" "${CONTAINER_IMAGE_TAG}" "$restart_policy" |
| 702 | elif [ "$runtime_type" = "podman" ]; then | 857 | elif [ "$runtime_type" = "podman" ]; then |
| 703 | generate_podman_service "$service_name" "${CONTAINER_IMAGE_NAME}" "${CONTAINER_IMAGE_TAG}" "$restart_policy" | 858 | generate_podman_service "$service_name" "${CONTAINER_IMAGE_NAME}" "${CONTAINER_IMAGE_TAG}" "$restart_policy" |
