diff options
| author | Bruce Ashfield <bruce.ashfield@gmail.com> | 2026-01-15 21:50:38 +0000 |
|---|---|---|
| committer | Bruce Ashfield <bruce.ashfield@gmail.com> | 2026-01-21 18:00:26 -0500 |
| commit | 02bce5b72e8725ba58d82627c780e376ac59a84b (patch) | |
| tree | 8e01635166b3d475fbf4fef34150ccde7ccda21a /recipes-containers/vcontainer/files | |
| parent | 640fc9278435c49b8c59d78c18d024c66b3d6e6a (diff) | |
| download | meta-virtualization-02bce5b72e8725ba58d82627c780e376ac59a84b.tar.gz | |
vcontainer: add multi-arch OCI support
Add functions to detect and handle multi-architecture OCI Image Index
format with automatic platform selection during import. Also add
oci-multiarch.bbclass for build-time multi-arch OCI creation.
Runtime support (vcontainer-common.sh):
- is_oci_image_index() - detect multi-arch OCI images
- get_oci_platforms() - list available platforms
- select_platform_manifest() - select manifest for target architecture
- extract_platform_oci() - extract single platform to new OCI dir
- normalize_arch_to_oci/from_oci() - architecture name mapping
- Update vimport to auto-select platform from multi-arch images
Build-time support (oci-multiarch.bbclass):
- Create OCI Image Index from multiconfig builds
- Collect images from vruntime-aarch64, vruntime-x86-64
- Combine blobs and create unified manifest list
Signed-off-by: Bruce Ashfield <bruce.ashfield@gmail.com>
Diffstat (limited to 'recipes-containers/vcontainer/files')
| -rwxr-xr-x | recipes-containers/vcontainer/files/vcontainer-common.sh | 243 |
1 files changed, 239 insertions, 4 deletions
diff --git a/recipes-containers/vcontainer/files/vcontainer-common.sh b/recipes-containers/vcontainer/files/vcontainer-common.sh index ddcb674f..39e57b13 100755 --- a/recipes-containers/vcontainer/files/vcontainer-common.sh +++ b/recipes-containers/vcontainer/files/vcontainer-common.sh | |||
| @@ -296,6 +296,196 @@ check_oci_arch() { | |||
| 296 | return 0 | 296 | return 0 |
| 297 | } | 297 | } |
| 298 | 298 | ||
| 299 | # ============================================================================ | ||
| 300 | # Multi-Architecture OCI Support | ||
| 301 | # ============================================================================ | ||
| 302 | # OCI Image Index (manifest list) format allows multiple platform-specific | ||
| 303 | # images under a single tag. These functions detect and handle multi-arch OCI. | ||
| 304 | # ============================================================================ | ||
| 305 | |||
| 306 | # Normalize architecture name to OCI convention | ||
| 307 | # Usage: normalize_arch_to_oci <arch> | ||
| 308 | # Returns: OCI-format architecture (arm64, amd64, etc.) | ||
| 309 | normalize_arch_to_oci() { | ||
| 310 | local arch="$1" | ||
| 311 | case "$arch" in | ||
| 312 | aarch64) echo "arm64" ;; | ||
| 313 | x86_64) echo "amd64" ;; | ||
| 314 | *) echo "$arch" ;; | ||
| 315 | esac | ||
| 316 | } | ||
| 317 | |||
| 318 | # Normalize OCI architecture to Yocto/Linux convention | ||
| 319 | # Usage: normalize_arch_from_oci <arch> | ||
| 320 | # Returns: Linux-format architecture (aarch64, x86_64, etc.) | ||
| 321 | normalize_arch_from_oci() { | ||
| 322 | local arch="$1" | ||
| 323 | case "$arch" in | ||
| 324 | arm64) echo "aarch64" ;; | ||
| 325 | amd64) echo "x86_64" ;; | ||
| 326 | *) echo "$arch" ;; | ||
| 327 | esac | ||
| 328 | } | ||
| 329 | |||
| 330 | # Check if OCI directory contains a multi-architecture Image Index | ||
| 331 | # Usage: is_oci_image_index <oci_dir> | ||
| 332 | # Returns: 0 if multi-arch, 1 if single-arch or not OCI | ||
| 333 | is_oci_image_index() { | ||
| 334 | local oci_dir="$1" | ||
| 335 | |||
| 336 | [ -f "$oci_dir/index.json" ] || return 1 | ||
| 337 | |||
| 338 | # Check if index.json has manifests with platform info | ||
| 339 | # Multi-arch images have "platform" object in manifest entries | ||
| 340 | if grep -q '"platform"' "$oci_dir/index.json" 2>/dev/null; then | ||
| 341 | # Also verify there are multiple manifests | ||
| 342 | local manifest_count=$(grep -c '"digest"' "$oci_dir/index.json" 2>/dev/null || echo "0") | ||
| 343 | [ "$manifest_count" -gt 1 ] && return 0 | ||
| 344 | |||
| 345 | # Single manifest with platform info is also a valid Image Index | ||
| 346 | # (could be a multi-arch image built with only one arch so far) | ||
| 347 | return 0 | ||
| 348 | fi | ||
| 349 | |||
| 350 | return 1 | ||
| 351 | } | ||
| 352 | |||
| 353 | # Get list of available platforms in a multi-arch OCI Image Index | ||
| 354 | # Usage: get_oci_platforms <oci_dir> | ||
| 355 | # Returns: Space-separated list of architectures (e.g., "arm64 amd64") | ||
| 356 | get_oci_platforms() { | ||
| 357 | local oci_dir="$1" | ||
| 358 | |||
| 359 | [ -f "$oci_dir/index.json" ] || return 1 | ||
| 360 | |||
| 361 | # Extract architecture values from platform objects | ||
| 362 | # Format: "platform": { "architecture": "arm64", "os": "linux" } | ||
| 363 | grep -o '"architecture"[[:space:]]*:[[:space:]]*"[^"]*"' "$oci_dir/index.json" 2>/dev/null | \ | ||
| 364 | sed 's/.*"\([^"]*\)"$/\1/' | \ | ||
| 365 | tr '\n' ' ' | sed 's/ $//' | ||
| 366 | } | ||
| 367 | |||
| 368 | # Select manifest digest for a specific platform from OCI Image Index | ||
| 369 | # Usage: select_platform_manifest <oci_dir> <target_arch> | ||
| 370 | # Returns: sha256 digest of the matching manifest (without "sha256:" prefix) | ||
| 371 | # Sets OCI_SELECTED_PLATFORM to the matched platform for informational purposes | ||
| 372 | select_platform_manifest() { | ||
| 373 | local oci_dir="$1" | ||
| 374 | local target_arch="$2" | ||
| 375 | |||
| 376 | [ -f "$oci_dir/index.json" ] || return 1 | ||
| 377 | |||
| 378 | # Normalize target arch to OCI convention | ||
| 379 | local oci_arch=$(normalize_arch_to_oci "$target_arch") | ||
| 380 | |||
| 381 | # Parse index.json to find manifest with matching platform | ||
| 382 | # This is done without jq using grep/sed for portability | ||
| 383 | local in_manifest=0 | ||
| 384 | local current_digest="" | ||
| 385 | local current_arch="" | ||
| 386 | local matched_digest="" | ||
| 387 | |||
| 388 | # Read index.json line by line | ||
| 389 | while IFS= read -r line; do | ||
| 390 | # Track when we're inside a manifest entry | ||
| 391 | if echo "$line" | grep -q '"manifests"'; then | ||
| 392 | in_manifest=1 | ||
| 393 | continue | ||
| 394 | fi | ||
| 395 | |||
| 396 | if [ "$in_manifest" = "1" ]; then | ||
| 397 | # Extract digest | ||
| 398 | if echo "$line" | grep -q '"digest"'; then | ||
| 399 | current_digest=$(echo "$line" | sed 's/.*"sha256:\([a-f0-9]*\)".*/\1/') | ||
| 400 | fi | ||
| 401 | |||
| 402 | # Extract architecture from platform | ||
| 403 | # Handle both formats: "architecture": "arm64" or {"architecture": "arm64", ...} | ||
| 404 | if echo "$line" | grep -q '"architecture"'; then | ||
| 405 | current_arch=$(echo "$line" | sed 's/.*"architecture"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/') | ||
| 406 | |||
| 407 | # Check if this matches our target | ||
| 408 | if [ "$current_arch" = "$oci_arch" ]; then | ||
| 409 | matched_digest="$current_digest" | ||
| 410 | OCI_SELECTED_PLATFORM="$current_arch" | ||
| 411 | break | ||
| 412 | fi | ||
| 413 | fi | ||
| 414 | |||
| 415 | # Reset on closing brace (end of manifest entry) | ||
| 416 | if echo "$line" | grep -q '^[[:space:]]*}'; then | ||
| 417 | current_digest="" | ||
| 418 | current_arch="" | ||
| 419 | fi | ||
| 420 | fi | ||
| 421 | done < "$oci_dir/index.json" | ||
| 422 | |||
| 423 | if [ -n "$matched_digest" ]; then | ||
| 424 | echo "$matched_digest" | ||
| 425 | return 0 | ||
| 426 | fi | ||
| 427 | |||
| 428 | return 1 | ||
| 429 | } | ||
| 430 | |||
| 431 | # Extract a single platform from multi-arch OCI to a new OCI directory | ||
| 432 | # Usage: extract_platform_oci <src_oci_dir> <dest_oci_dir> <manifest_digest> | ||
| 433 | # This creates a single-arch OCI directory that skopeo can import | ||
| 434 | extract_platform_oci() { | ||
| 435 | local src_dir="$1" | ||
| 436 | local dest_dir="$2" | ||
| 437 | local manifest_digest="$3" | ||
| 438 | |||
| 439 | mkdir -p "$dest_dir/blobs/sha256" | ||
| 440 | |||
| 441 | # Copy the manifest blob | ||
| 442 | cp "$src_dir/blobs/sha256/$manifest_digest" "$dest_dir/blobs/sha256/" | ||
| 443 | |||
| 444 | # Read the manifest to get config and layer digests | ||
| 445 | local manifest_file="$src_dir/blobs/sha256/$manifest_digest" | ||
| 446 | |||
| 447 | # Extract and copy config blob | ||
| 448 | local config_digest=$(grep -o '"config"[[:space:]]*:[[:space:]]*{[^}]*"digest"[[:space:]]*:[[:space:]]*"sha256:[a-f0-9]*"' "$manifest_file" | \ | ||
| 449 | sed 's/.*sha256:\([a-f0-9]*\)".*/\1/') | ||
| 450 | if [ -n "$config_digest" ] && [ -f "$src_dir/blobs/sha256/$config_digest" ]; then | ||
| 451 | cp "$src_dir/blobs/sha256/$config_digest" "$dest_dir/blobs/sha256/" | ||
| 452 | fi | ||
| 453 | |||
| 454 | # Extract and copy layer blobs | ||
| 455 | grep -o '"digest"[[:space:]]*:[[:space:]]*"sha256:[a-f0-9]*"' "$manifest_file" | \ | ||
| 456 | sed 's/.*sha256:\([a-f0-9]*\)".*/\1/' | while read -r layer_digest; do | ||
| 457 | if [ -f "$src_dir/blobs/sha256/$layer_digest" ]; then | ||
| 458 | cp "$src_dir/blobs/sha256/$layer_digest" "$dest_dir/blobs/sha256/" | ||
| 459 | fi | ||
| 460 | done | ||
| 461 | |||
| 462 | # Get manifest size | ||
| 463 | local manifest_size=$(stat -c%s "$manifest_file" 2>/dev/null || stat -f%z "$manifest_file" 2>/dev/null) | ||
| 464 | |||
| 465 | # Create new index.json pointing to just this manifest | ||
| 466 | cat > "$dest_dir/index.json" << EOF | ||
| 467 | { | ||
| 468 | "schemaVersion": 2, | ||
| 469 | "manifests": [ | ||
| 470 | { | ||
| 471 | "mediaType": "application/vnd.oci.image.manifest.v1+json", | ||
| 472 | "digest": "sha256:$manifest_digest", | ||
| 473 | "size": $manifest_size | ||
| 474 | } | ||
| 475 | ] | ||
| 476 | } | ||
| 477 | EOF | ||
| 478 | |||
| 479 | # Copy oci-layout | ||
| 480 | if [ -f "$src_dir/oci-layout" ]; then | ||
| 481 | cp "$src_dir/oci-layout" "$dest_dir/" | ||
| 482 | else | ||
| 483 | echo '{"imageLayoutVersion": "1.0.0"}' > "$dest_dir/oci-layout" | ||
| 484 | fi | ||
| 485 | |||
| 486 | return 0 | ||
| 487 | } | ||
| 488 | |||
| 299 | show_usage() { | 489 | show_usage() { |
| 300 | local PROG_NAME=$(basename "$0") | 490 | local PROG_NAME=$(basename "$0") |
| 301 | local RUNTIME_UPPER=$(echo "$VCONTAINER_RUNTIME_CMD" | sed 's/./\U&/') | 491 | local RUNTIME_UPPER=$(echo "$VCONTAINER_RUNTIME_CMD" | sed 's/./\U&/') |
| @@ -348,6 +538,7 @@ ${BOLD}${RUNTIME_UPPER}-COMPATIBLE COMMANDS:${NC} | |||
| 348 | 538 | ||
| 349 | ${BOLD}EXTENDED COMMANDS (${VCONTAINER_RUNTIME_NAME}-specific):${NC} | 539 | ${BOLD}EXTENDED COMMANDS (${VCONTAINER_RUNTIME_NAME}-specific):${NC} |
| 350 | ${CYAN}vimport${NC} <path> [name:tag] Import from OCI dir, tarball, or directory (auto-detect) | 540 | ${CYAN}vimport${NC} <path> [name:tag] Import from OCI dir, tarball, or directory (auto-detect) |
| 541 | Multi-arch OCI Image Index supported (auto-selects platform) | ||
| 351 | ${CYAN}vrun${NC} [opts] <image> [cmd] Run command, clearing entrypoint (see RUN vs VRUN below) | 542 | ${CYAN}vrun${NC} [opts] <image> [cmd] Run command, clearing entrypoint (see RUN vs VRUN below) |
| 352 | ${CYAN}vstorage${NC} List all storage directories (alias: vstorage list) | 543 | ${CYAN}vstorage${NC} List all storage directories (alias: vstorage list) |
| 353 | ${CYAN}vstorage list${NC} List all storage directories with details | 544 | ${CYAN}vstorage list${NC} List all storage directories with details |
| @@ -425,6 +616,7 @@ ${BOLD}GLOBAL OPTIONS:${NC} | |||
| 425 | --no-kvm Disable KVM acceleration (use TCG emulation) | 616 | --no-kvm Disable KVM acceleration (use TCG emulation) |
| 426 | --no-daemon Run in ephemeral mode (don't auto-start/use daemon) | 617 | --no-daemon Run in ephemeral mode (don't auto-start/use daemon) |
| 427 | --registry <url> Default registry for unqualified images (e.g., 10.0.2.2:5000/yocto) | 618 | --registry <url> Default registry for unqualified images (e.g., 10.0.2.2:5000/yocto) |
| 619 | --no-registry Disable baked-in default registry (use images as-is) | ||
| 428 | --insecure-registry <host:port> Mark registry as insecure (HTTP). Can repeat. | 620 | --insecure-registry <host:port> Mark registry as insecure (HTTP). Can repeat. |
| 429 | --verbose, -v Enable verbose output | 621 | --verbose, -v Enable verbose output |
| 430 | --help, -h Show this help | 622 | --help, -h Show this help |
| @@ -635,6 +827,11 @@ while [ $# -gt 0 ]; do | |||
| 635 | REGISTRY="$2" | 827 | REGISTRY="$2" |
| 636 | shift 2 | 828 | shift 2 |
| 637 | ;; | 829 | ;; |
| 830 | --no-registry) | ||
| 831 | # Explicitly disable baked-in registry (passes docker_registry=none to init) | ||
| 832 | REGISTRY="none" | ||
| 833 | shift | ||
| 834 | ;; | ||
| 638 | --insecure-registry) | 835 | --insecure-registry) |
| 639 | INSECURE_REGISTRIES+=("$2") | 836 | INSECURE_REGISTRIES+=("$2") |
| 640 | shift 2 | 837 | shift 2 |
| @@ -1322,15 +1519,53 @@ case "$COMMAND" in | |||
| 1322 | if [ -d "$INPUT_PATH" ]; then | 1519 | if [ -d "$INPUT_PATH" ]; then |
| 1323 | if [ -f "$INPUT_PATH/index.json" ] || [ -f "$INPUT_PATH/oci-layout" ]; then | 1520 | if [ -f "$INPUT_PATH/index.json" ] || [ -f "$INPUT_PATH/oci-layout" ]; then |
| 1324 | INPUT_TYPE="oci" | 1521 | INPUT_TYPE="oci" |
| 1522 | ACTUAL_OCI_PATH="$INPUT_PATH" | ||
| 1523 | |||
| 1524 | # Check for multi-architecture OCI Image Index | ||
| 1525 | if is_oci_image_index "$INPUT_PATH"; then | ||
| 1526 | local available_platforms=$(get_oci_platforms "$INPUT_PATH") | ||
| 1527 | [ "$VERBOSE" = "true" ] && echo -e "${CYAN}[$VCONTAINER_RUNTIME_NAME]${NC} Multi-arch OCI detected. Available: $available_platforms" >&2 | ||
| 1528 | |||
| 1529 | # Select manifest for target architecture | ||
| 1530 | local manifest_digest=$(select_platform_manifest "$INPUT_PATH" "$TARGET_ARCH") | ||
| 1531 | if [ -z "$manifest_digest" ]; then | ||
| 1532 | echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} Architecture $TARGET_ARCH not found in multi-arch image" >&2 | ||
| 1533 | echo -e "${YELLOW}[$VCONTAINER_RUNTIME_NAME]${NC} Available platforms: $available_platforms" >&2 | ||
| 1534 | echo -e "${YELLOW}[$VCONTAINER_RUNTIME_NAME]${NC} Use --arch <arch> to select a different platform" >&2 | ||
| 1535 | exit 1 | ||
| 1536 | fi | ||
| 1325 | 1537 | ||
| 1326 | # Check architecture before importing | 1538 | echo -e "${GREEN}[$VCONTAINER_RUNTIME_NAME]${NC} Selected platform: $OCI_SELECTED_PLATFORM (from multi-arch image)" >&2 |
| 1327 | if ! check_oci_arch "$INPUT_PATH" "$TARGET_ARCH"; then | 1539 | |
| 1328 | exit 1 | 1540 | # Extract single-platform OCI to temp directory |
| 1541 | TEMP_OCI_DIR=$(mktemp -d) | ||
| 1542 | trap "rm -rf '$TEMP_OCI_DIR'" EXIT | ||
| 1543 | |||
| 1544 | if ! extract_platform_oci "$INPUT_PATH" "$TEMP_OCI_DIR" "$manifest_digest"; then | ||
| 1545 | echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} Failed to extract platform from multi-arch image" >&2 | ||
| 1546 | exit 1 | ||
| 1547 | fi | ||
| 1548 | |||
| 1549 | ACTUAL_OCI_PATH="$TEMP_OCI_DIR" | ||
| 1550 | [ "$VERBOSE" = "true" ] && echo -e "${CYAN}[$VCONTAINER_RUNTIME_NAME]${NC} Extracted to: $TEMP_OCI_DIR" >&2 | ||
| 1551 | else | ||
| 1552 | # Single-arch OCI - check architecture before importing | ||
| 1553 | if ! check_oci_arch "$INPUT_PATH" "$TARGET_ARCH"; then | ||
| 1554 | exit 1 | ||
| 1555 | fi | ||
| 1329 | fi | 1556 | fi |
| 1330 | 1557 | ||
| 1331 | # Use skopeo to properly import OCI image with full metadata (entrypoint, cmd, etc.) | 1558 | # Use skopeo to properly import OCI image with full metadata (entrypoint, cmd, etc.) |
| 1332 | # This preserves the container config unlike raw import | 1559 | # This preserves the container config unlike raw import |
| 1333 | RUNTIME_CMD="skopeo copy oci:{INPUT} ${VCONTAINER_IMPORT_TARGET}$IMAGE_NAME && $VCONTAINER_RUNTIME_CMD images" | 1560 | # For multi-arch, we import from the extracted temp directory |
| 1561 | if [ "$ACTUAL_OCI_PATH" = "$INPUT_PATH" ]; then | ||
| 1562 | RUNTIME_CMD="skopeo copy oci:{INPUT} ${VCONTAINER_IMPORT_TARGET}$IMAGE_NAME && $VCONTAINER_RUNTIME_CMD images" | ||
| 1563 | else | ||
| 1564 | # Multi-arch: copy extracted OCI to share dir for import | ||
| 1565 | # We need to handle this specially since INPUT_PATH differs from actual OCI | ||
| 1566 | INPUT_PATH="$ACTUAL_OCI_PATH" | ||
| 1567 | RUNTIME_CMD="skopeo copy oci:{INPUT} ${VCONTAINER_IMPORT_TARGET}$IMAGE_NAME && $VCONTAINER_RUNTIME_CMD images" | ||
| 1568 | fi | ||
| 1334 | else | 1569 | else |
| 1335 | # Directory but not OCI - check if it looks like a deploy/images dir | 1570 | # Directory but not OCI - check if it looks like a deploy/images dir |
| 1336 | # and provide a helpful hint | 1571 | # and provide a helpful hint |
