diff options
| author | Bruce Ashfield <bruce.ashfield@gmail.com> | 2026-01-15 21:50:27 +0000 |
|---|---|---|
| committer | Bruce Ashfield <bruce.ashfield@gmail.com> | 2026-02-09 03:32:52 +0000 |
| commit | 1c902059df8215731e9ee7210f6bbc4e19593761 (patch) | |
| tree | 2bec4fc2d24b3eb273b450016021e98d7f89a7af /recipes-containers | |
| parent | 1008fa9b63f5123989e9f82cd50d51b60bfee2ee (diff) | |
| download | meta-virtualization-1c902059df8215731e9ee7210f6bbc4e19593761.tar.gz | |
container-registry: abstract config and add multi-directory push
Abstract registry configuration for Docker/Podman compatibility and add
multi-directory scanning for easy multi-arch manifest list creation.
- Support both DOCKER_REGISTRY_INSECURE and CONTAINER_REGISTRY_INSECURE
- Add DEPLOY_DIR_IMAGES to scan all machine directories
- Support push by path (single OCI) and push by name (all archs)
- Add environment variable overrides for flexibility
- Single 'push' command now creates multi-arch manifest lists
Signed-off-by: Bruce Ashfield <bruce.ashfield@gmail.com>
Diffstat (limited to 'recipes-containers')
| -rw-r--r-- | recipes-containers/container-registry/container-registry-index.bb | 373 | ||||
| -rw-r--r-- | recipes-containers/container-registry/docker-registry-config.bb | 24 |
2 files changed, 352 insertions, 45 deletions
diff --git a/recipes-containers/container-registry/container-registry-index.bb b/recipes-containers/container-registry/container-registry-index.bb index 75090faa..29125eee 100644 --- a/recipes-containers/container-registry/container-registry-index.bb +++ b/recipes-containers/container-registry/container-registry-index.bb | |||
| @@ -98,6 +98,8 @@ python do_generate_registry_script() { | |||
| 98 | script_path = d.getVar('CONTAINER_REGISTRY_SCRIPT') | 98 | script_path = d.getVar('CONTAINER_REGISTRY_SCRIPT') |
| 99 | deploy_dir = d.getVar('DEPLOY_DIR') | 99 | deploy_dir = d.getVar('DEPLOY_DIR') |
| 100 | deploy_dir_image = d.getVar('DEPLOY_DIR_IMAGE') | 100 | deploy_dir_image = d.getVar('DEPLOY_DIR_IMAGE') |
| 101 | # Parent of DEPLOY_DIR_IMAGE (e.g., tmp/deploy/images/) for multi-arch discovery | ||
| 102 | deploy_dir_images = os.path.dirname(deploy_dir_image) | ||
| 101 | 103 | ||
| 102 | # Find registry binary path | 104 | # Find registry binary path |
| 103 | native_sysroot = d.getVar('STAGING_DIR_NATIVE') or '' | 105 | native_sysroot = d.getVar('STAGING_DIR_NATIVE') or '' |
| @@ -160,7 +162,12 @@ REGISTRY_CONFIG="{config_file}" | |||
| 160 | REGISTRY_STORAGE="{registry_storage}" | 162 | REGISTRY_STORAGE="{registry_storage}" |
| 161 | REGISTRY_URL="{registry_url}" | 163 | REGISTRY_URL="{registry_url}" |
| 162 | REGISTRY_NAMESPACE="{registry_namespace}" | 164 | REGISTRY_NAMESPACE="{registry_namespace}" |
| 163 | DEPLOY_DIR_IMAGE="{deploy_dir_image}" | 165 | |
| 166 | # Deploy directories - can be overridden via environment | ||
| 167 | # DEPLOY_DIR_IMAGES: parent directory containing per-machine deploy dirs | ||
| 168 | # DEPLOY_DIR_IMAGE: single machine deploy dir (legacy, still supported) | ||
| 169 | DEPLOY_DIR_IMAGES="${{DEPLOY_DIR_IMAGES:-{deploy_dir_images}}}" | ||
| 170 | DEPLOY_DIR_IMAGE="${{DEPLOY_DIR_IMAGE:-{deploy_dir_image}}}" | ||
| 164 | 171 | ||
| 165 | # Baked-in defaults from bitbake (can be overridden by CLI or env vars) | 172 | # Baked-in defaults from bitbake (can be overridden by CLI or env vars) |
| 166 | DEFAULT_TAG_STRATEGY="{tag_strategy}" | 173 | DEFAULT_TAG_STRATEGY="{tag_strategy}" |
| @@ -299,6 +306,189 @@ cmd_status() {{ | |||
| 299 | fi | 306 | fi |
| 300 | }} | 307 | }} |
| 301 | 308 | ||
| 309 | # ============================================================================ | ||
| 310 | # Multi-Architecture Manifest List Support | ||
| 311 | # ============================================================================ | ||
| 312 | # Always creates/updates manifest lists for tags, enabling multi-arch images. | ||
| 313 | # When pushing the same image name from different architectures, each push | ||
| 314 | # adds to the manifest list instead of overwriting. | ||
| 315 | # ============================================================================ | ||
| 316 | |||
| 317 | # Get architecture from OCI image config | ||
| 318 | # Usage: get_oci_arch <oci_dir> | ||
| 319 | get_oci_arch() {{ | ||
| 320 | local oci_dir="$1" | ||
| 321 | [ -f "$oci_dir/index.json" ] || return 1 | ||
| 322 | |||
| 323 | # Get manifest digest from index.json | ||
| 324 | local manifest_digest=$(grep -o '"digest"[[:space:]]*:[[:space:]]*"sha256:[a-f0-9]*"' "$oci_dir/index.json" | head -1 | sed 's/.*sha256:\\([a-f0-9]*\\)".*/\\1/') | ||
| 325 | [ -z "$manifest_digest" ] && return 1 | ||
| 326 | |||
| 327 | # Get config digest from manifest | ||
| 328 | local manifest_file="$oci_dir/blobs/sha256/$manifest_digest" | ||
| 329 | [ -f "$manifest_file" ] || return 1 | ||
| 330 | local config_digest=$(grep -o '"config"[^}}]*"digest"[[:space:]]*:[[:space:]]*"sha256:[a-f0-9]*"' "$manifest_file" | sed 's/.*sha256:\\([a-f0-9]*\\)".*/\\1/') | ||
| 331 | [ -z "$config_digest" ] && return 1 | ||
| 332 | |||
| 333 | # Get architecture from config | ||
| 334 | local config_file="$oci_dir/blobs/sha256/$config_digest" | ||
| 335 | [ -f "$config_file" ] || return 1 | ||
| 336 | grep -o '"architecture"[[:space:]]*:[[:space:]]*"[^"]*"' "$config_file" | head -1 | sed 's/.*"\\([^"]*\\)"$/\\1/' | ||
| 337 | }} | ||
| 338 | |||
| 339 | # Check if a tag points to a manifest list (vs single manifest) | ||
| 340 | # Usage: is_manifest_list <image_ref> | ||
| 341 | # Returns: 0 if manifest list, 1 if single manifest or not found | ||
| 342 | is_manifest_list() {{ | ||
| 343 | local image="$1" | ||
| 344 | local tag="$2" | ||
| 345 | |||
| 346 | local content_type=$(curl -s -I -H "Accept: application/vnd.oci.image.index.v1+json, application/vnd.docker.distribution.manifest.list.v2+json" \\ | ||
| 347 | "http://$REGISTRY_URL/v2/$image/manifests/$tag" 2>/dev/null | grep -i "content-type" | head -1) | ||
| 348 | |||
| 349 | echo "$content_type" | grep -qE "manifest.list|image.index" | ||
| 350 | }} | ||
| 351 | |||
| 352 | # Get existing manifest list for a tag (if any) | ||
| 353 | # Usage: get_manifest_list <image> <tag> | ||
| 354 | # Returns: JSON manifest list or empty string | ||
| 355 | get_manifest_list() {{ | ||
| 356 | local image="$1" | ||
| 357 | local tag="$2" | ||
| 358 | |||
| 359 | curl -s -H "Accept: application/vnd.oci.image.index.v1+json, application/vnd.docker.distribution.manifest.list.v2+json" \\ | ||
| 360 | "http://$REGISTRY_URL/v2/$image/manifests/$tag" 2>/dev/null | ||
| 361 | }} | ||
| 362 | |||
| 363 | # Get manifest digest and size for an image by tag | ||
| 364 | # Usage: get_manifest_info <image> <tag> | ||
| 365 | # Returns: digest:size or empty | ||
| 366 | get_manifest_info() {{ | ||
| 367 | local image="$1" | ||
| 368 | local tag="$2" | ||
| 369 | |||
| 370 | local headers=$(curl -s -I -H "Accept: application/vnd.oci.image.manifest.v1+json, application/vnd.docker.distribution.manifest.v2+json" \\ | ||
| 371 | "http://$REGISTRY_URL/v2/$image/manifests/$tag" 2>/dev/null) | ||
| 372 | |||
| 373 | local digest=$(echo "$headers" | grep -i "docker-content-digest" | awk '{{print $2}}' | tr -d '\\r\\n') | ||
| 374 | local size=$(echo "$headers" | grep -i "content-length" | awk '{{print $2}}' | tr -d '\\r\\n') | ||
| 375 | |||
| 376 | [ -n "$digest" ] && [ -n "$size" ] && echo "$digest:$size" | ||
| 377 | }} | ||
| 378 | |||
| 379 | # Push image by digest (returns the digest) | ||
| 380 | # Usage: push_by_digest <oci_dir> <image_name> | ||
| 381 | push_by_digest() {{ | ||
| 382 | local oci_dir="$1" | ||
| 383 | local image_name="$2" | ||
| 384 | local temp_tag="temp-${{RANDOM}}-$(date +%s)" | ||
| 385 | |||
| 386 | # Push with temporary tag | ||
| 387 | "$SKOPEO_BIN" copy --dest-tls-verify=false \\ | ||
| 388 | "oci:$oci_dir" \\ | ||
| 389 | "docker://$REGISTRY_URL/$REGISTRY_NAMESPACE/$image_name:$temp_tag" >/dev/null 2>&1 | ||
| 390 | |||
| 391 | # Get digest for the pushed image | ||
| 392 | local info=$(get_manifest_info "$REGISTRY_NAMESPACE/$image_name" "$temp_tag") | ||
| 393 | local digest=$(echo "$info" | cut -d: -f1-2) # sha256:xxx | ||
| 394 | local size=$(echo "$info" | cut -d: -f3) | ||
| 395 | |||
| 396 | # Delete the temp tag (leave the blobs) | ||
| 397 | curl -s -X DELETE "http://$REGISTRY_URL/v2/$REGISTRY_NAMESPACE/$image_name/manifests/$temp_tag" >/dev/null 2>&1 || true | ||
| 398 | |||
| 399 | echo "$digest:$size" | ||
| 400 | }} | ||
| 401 | |||
| 402 | # Create or update manifest list for a tag | ||
| 403 | # Usage: update_manifest_list <image> <tag> <new_digest> <new_size> <new_arch> | ||
| 404 | update_manifest_list() {{ | ||
| 405 | local image="$1" | ||
| 406 | local tag="$2" | ||
| 407 | local new_digest="$3" | ||
| 408 | local new_size="$4" | ||
| 409 | local new_arch="$5" | ||
| 410 | local new_os="${{6:-linux}}" | ||
| 411 | |||
| 412 | # Normalize architecture for OCI | ||
| 413 | case "$new_arch" in | ||
| 414 | aarch64) new_arch="arm64" ;; | ||
| 415 | x86_64) new_arch="amd64" ;; | ||
| 416 | esac | ||
| 417 | |||
| 418 | local manifests="" | ||
| 419 | |||
| 420 | # Check for existing manifest list or single manifest | ||
| 421 | if is_manifest_list "$image" "$tag"; then | ||
| 422 | # Get existing manifest list and extract manifests (excluding our arch) | ||
| 423 | local existing=$(get_manifest_list "$image" "$tag") | ||
| 424 | manifests=$(echo "$existing" | python3 -c " | ||
| 425 | import sys, json | ||
| 426 | try: | ||
| 427 | data = json.load(sys.stdin) | ||
| 428 | for m in data.get('manifests', []): | ||
| 429 | p = m.get('platform', {{}}) | ||
| 430 | if p.get('architecture') != '$new_arch': | ||
| 431 | print(json.dumps(m)) | ||
| 432 | except: pass | ||
| 433 | " 2>/dev/null) | ||
| 434 | else | ||
| 435 | # Check if there's a single manifest at this tag | ||
| 436 | local existing_info=$(get_manifest_info "$image" "$tag") | ||
| 437 | if [ -n "$existing_info" ]; then | ||
| 438 | # Get architecture of existing single manifest | ||
| 439 | local existing_digest=$(echo "$existing_info" | cut -d: -f1-2) | ||
| 440 | local existing_size=$(echo "$existing_info" | cut -d: -f3) | ||
| 441 | |||
| 442 | # Inspect to get architecture | ||
| 443 | local existing_arch=$("$SKOPEO_BIN" inspect --tls-verify=false \\ | ||
| 444 | "docker://$REGISTRY_URL/$image:$tag" 2>/dev/null | \\ | ||
| 445 | python3 -c "import sys,json; print(json.load(sys.stdin).get('Architecture',''))" 2>/dev/null) | ||
| 446 | |||
| 447 | if [ -n "$existing_arch" ] && [ "$existing_arch" != "$new_arch" ]; then | ||
| 448 | # Different arch - include it in manifest list | ||
| 449 | manifests=$(cat <<MANIFEST | ||
| 450 | {{"mediaType": "application/vnd.oci.image.manifest.v1+json", "digest": "$existing_digest", "size": $existing_size, "platform": {{"architecture": "$existing_arch", "os": "linux"}}}} | ||
| 451 | MANIFEST | ||
| 452 | ) | ||
| 453 | fi | ||
| 454 | fi | ||
| 455 | fi | ||
| 456 | |||
| 457 | # Add our new manifest | ||
| 458 | local new_manifest='{{"mediaType": "application/vnd.oci.image.manifest.v1+json", "digest": "'$new_digest'", "size": '$new_size', "platform": {{"architecture": "'$new_arch'", "os": "'$new_os'"}}}}' | ||
| 459 | |||
| 460 | if [ -n "$manifests" ]; then | ||
| 461 | manifests="$manifests | ||
| 462 | $new_manifest" | ||
| 463 | else | ||
| 464 | manifests="$new_manifest" | ||
| 465 | fi | ||
| 466 | |||
| 467 | # Create manifest list JSON | ||
| 468 | local manifest_list=$(python3 -c " | ||
| 469 | import sys, json | ||
| 470 | manifests = [] | ||
| 471 | for line in sys.stdin: | ||
| 472 | line = line.strip() | ||
| 473 | if line: | ||
| 474 | manifests.append(json.loads(line)) | ||
| 475 | result = {{ | ||
| 476 | 'schemaVersion': 2, | ||
| 477 | 'mediaType': 'application/vnd.oci.image.index.v1+json', | ||
| 478 | 'manifests': manifests | ||
| 479 | }} | ||
| 480 | print(json.dumps(result, indent=2)) | ||
| 481 | " <<< "$manifests") | ||
| 482 | |||
| 483 | # Push manifest list | ||
| 484 | local status=$(curl -s -o /dev/null -w "%{{http_code}}" -X PUT \\ | ||
| 485 | -H "Content-Type: application/vnd.oci.image.index.v1+json" \\ | ||
| 486 | -d "$manifest_list" \\ | ||
| 487 | "http://$REGISTRY_URL/v2/$image/manifests/$tag") | ||
| 488 | |||
| 489 | [ "$status" = "201" ] || [ "$status" = "200" ] | ||
| 490 | }} | ||
| 491 | |||
| 302 | cmd_push() {{ | 492 | cmd_push() {{ |
| 303 | shift # Remove 'push' from args | 493 | shift # Remove 'push' from args |
| 304 | 494 | ||
| @@ -367,59 +557,146 @@ cmd_push() {{ | |||
| 367 | tags=$(generate_tags "$strategy") | 557 | tags=$(generate_tags "$strategy") |
| 368 | fi | 558 | fi |
| 369 | 559 | ||
| 560 | # Check if argument is a path to an OCI directory (contains / or ends with -oci) | ||
| 561 | if [ -n "$image_filter" ] && [ -d "$image_filter" ] && [ -f "$image_filter/index.json" ]; then | ||
| 562 | # Direct path mode: push single OCI directory | ||
| 563 | local oci_dir="$image_filter" | ||
| 564 | local name=$(basename "$oci_dir" | sed 's/-latest-oci$//' | sed 's/-oci$//') | ||
| 565 | name=$(echo "$name" | sed 's/-qemux86-64//' | sed 's/-qemuarm64//') | ||
| 566 | name=$(echo "$name" | sed 's/\\.rootfs-[0-9]*//') | ||
| 567 | |||
| 568 | local arch=$(get_oci_arch "$oci_dir") | ||
| 569 | [ -z "$arch" ] && arch="unknown" | ||
| 570 | |||
| 571 | echo "Pushing OCI directory: $oci_dir" | ||
| 572 | echo " Image name: $name ($arch)" | ||
| 573 | echo " To registry: $REGISTRY_URL/$REGISTRY_NAMESPACE/" | ||
| 574 | echo " Tags: $tags" | ||
| 575 | echo "" | ||
| 576 | |||
| 577 | echo " Uploading image blobs..." | ||
| 578 | local digest_info=$(push_by_digest "$oci_dir" "$name") | ||
| 579 | local digest=$(echo "$digest_info" | cut -d: -f1-2) | ||
| 580 | local size=$(echo "$digest_info" | cut -d: -f3) | ||
| 581 | |||
| 582 | if [ -z "$digest" ]; then | ||
| 583 | echo " ERROR: Failed to push image" | ||
| 584 | return 1 | ||
| 585 | fi | ||
| 586 | |||
| 587 | echo " Image digest: $digest" | ||
| 588 | |||
| 589 | for tag in $tags; do | ||
| 590 | echo " Creating/updating manifest list: $tag" | ||
| 591 | if update_manifest_list "$REGISTRY_NAMESPACE/$name" "$tag" "$digest" "$size" "$arch"; then | ||
| 592 | echo " -> $REGISTRY_URL/$REGISTRY_NAMESPACE/$name:$tag (manifest list)" | ||
| 593 | else | ||
| 594 | echo " WARNING: Failed to update manifest list, falling back to direct push" | ||
| 595 | "$SKOPEO_BIN" copy --dest-tls-verify=false \\ | ||
| 596 | "oci:$oci_dir" \\ | ||
| 597 | "docker://$REGISTRY_URL/$REGISTRY_NAMESPACE/$name:$tag" | ||
| 598 | fi | ||
| 599 | done | ||
| 600 | |||
| 601 | echo "" | ||
| 602 | echo "Done." | ||
| 603 | return 0 | ||
| 604 | fi | ||
| 605 | |||
| 606 | # Name filter mode or push all: scan machine directories | ||
| 370 | if [ -n "$image_filter" ]; then | 607 | if [ -n "$image_filter" ]; then |
| 371 | echo "Pushing image: $image_filter" | 608 | echo "Pushing image: $image_filter (all architectures)" |
| 372 | else | 609 | else |
| 373 | echo "Pushing all OCI images from $DEPLOY_DIR_IMAGE" | 610 | echo "Pushing all OCI images" |
| 374 | fi | 611 | fi |
| 612 | echo "Scanning: $DEPLOY_DIR_IMAGES/*/" | ||
| 375 | echo "To registry: $REGISTRY_URL/$REGISTRY_NAMESPACE/" | 613 | echo "To registry: $REGISTRY_URL/$REGISTRY_NAMESPACE/" |
| 376 | echo "Tags: $tags" | 614 | echo "Tags: $tags" |
| 615 | echo "(Multi-arch manifest lists enabled)" | ||
| 377 | echo "" | 616 | echo "" |
| 378 | 617 | ||
| 379 | local found=0 | 618 | local found=0 |
| 380 | for oci_dir in "$DEPLOY_DIR_IMAGE"/*-oci; do | ||
| 381 | [ -d "$oci_dir" ] || continue | ||
| 382 | [ -f "$oci_dir/index.json" ] || continue | ||
| 383 | 619 | ||
| 384 | name=$(basename "$oci_dir" | sed 's/-latest-oci$//' | sed 's/-oci$//') | 620 | # Iterate over all machine directories (e.g., qemuarm64, qemux86-64) |
| 385 | # Remove machine suffix | 621 | for machine_dir in "$DEPLOY_DIR_IMAGES"/*/; do |
| 386 | name=$(echo "$name" | sed 's/-qemux86-64//' | sed 's/-qemuarm64//') | 622 | [ -d "$machine_dir" ] || continue |
| 387 | # Remove rootfs timestamp | ||
| 388 | name=$(echo "$name" | sed 's/\\.rootfs-[0-9]*//') | ||
| 389 | 623 | ||
| 390 | # Filter by image name if specified | 624 | local machine_name=$(basename "$machine_dir") |
| 391 | if [ -n "$image_filter" ]; then | ||
| 392 | # Match exact name or name.rootfs variant | ||
| 393 | case "$name" in | ||
| 394 | "$image_filter"|"$image_filter.rootfs") | ||
| 395 | : # match | ||
| 396 | ;; | ||
| 397 | *) | ||
| 398 | continue | ||
| 399 | ;; | ||
| 400 | esac | ||
| 401 | fi | ||
| 402 | 625 | ||
| 403 | found=1 | 626 | # Find OCI directories in this machine's deploy dir |
| 404 | echo "Pushing: $name" | 627 | for oci_dir in "$machine_dir"*-oci; do |
| 405 | for tag in $tags; do | 628 | [ -d "$oci_dir" ] || continue |
| 406 | echo " -> $REGISTRY_URL/$REGISTRY_NAMESPACE/$name:$tag" | 629 | [ -f "$oci_dir/index.json" ] || continue |
| 407 | "$SKOPEO_BIN" copy --dest-tls-verify=false \\ | 630 | |
| 408 | "oci:$oci_dir" \\ | 631 | name=$(basename "$oci_dir" | sed 's/-latest-oci$//' | sed 's/-oci$//') |
| 409 | "docker://$REGISTRY_URL/$REGISTRY_NAMESPACE/$name:$tag" | 632 | # Remove machine suffix |
| 633 | name=$(echo "$name" | sed 's/-qemux86-64//' | sed 's/-qemuarm64//') | ||
| 634 | # Remove rootfs timestamp | ||
| 635 | name=$(echo "$name" | sed 's/\\.rootfs-[0-9]*//') | ||
| 636 | |||
| 637 | # Filter by image name if specified | ||
| 638 | if [ -n "$image_filter" ]; then | ||
| 639 | # Match exact name or name.rootfs variant | ||
| 640 | case "$name" in | ||
| 641 | "$image_filter"|"$image_filter.rootfs") | ||
| 642 | : # match | ||
| 643 | ;; | ||
| 644 | *) | ||
| 645 | continue | ||
| 646 | ;; | ||
| 647 | esac | ||
| 648 | fi | ||
| 649 | |||
| 650 | found=1 | ||
| 651 | |||
| 652 | # Get architecture from OCI image | ||
| 653 | local arch=$(get_oci_arch "$oci_dir") | ||
| 654 | [ -z "$arch" ] && arch="unknown" | ||
| 655 | echo "Pushing: $name ($arch) [from $machine_name]" | ||
| 656 | |||
| 657 | # Push image by digest first | ||
| 658 | echo " Uploading image blobs..." | ||
| 659 | local digest_info=$(push_by_digest "$oci_dir" "$name") | ||
| 660 | local digest=$(echo "$digest_info" | cut -d: -f1-2) | ||
| 661 | local size=$(echo "$digest_info" | cut -d: -f3) | ||
| 662 | |||
| 663 | if [ -z "$digest" ]; then | ||
| 664 | echo " ERROR: Failed to push image" | ||
| 665 | continue | ||
| 666 | fi | ||
| 667 | |||
| 668 | echo " Image digest: $digest" | ||
| 669 | |||
| 670 | # Update manifest list for each tag | ||
| 671 | for tag in $tags; do | ||
| 672 | echo " Creating/updating manifest list: $tag" | ||
| 673 | if update_manifest_list "$REGISTRY_NAMESPACE/$name" "$tag" "$digest" "$size" "$arch"; then | ||
| 674 | echo " -> $REGISTRY_URL/$REGISTRY_NAMESPACE/$name:$tag (manifest list)" | ||
| 675 | else | ||
| 676 | echo " WARNING: Failed to update manifest list, falling back to direct push" | ||
| 677 | "$SKOPEO_BIN" copy --dest-tls-verify=false \\ | ||
| 678 | "oci:$oci_dir" \\ | ||
| 679 | "docker://$REGISTRY_URL/$REGISTRY_NAMESPACE/$name:$tag" | ||
| 680 | fi | ||
| 681 | done | ||
| 682 | echo "" | ||
| 410 | done | 683 | done |
| 411 | done | 684 | done |
| 412 | 685 | ||
| 413 | if [ -n "$image_filter" ] && [ "$found" = "0" ]; then | 686 | if [ -n "$image_filter" ] && [ "$found" = "0" ]; then |
| 414 | echo "Error: Image '$image_filter' not found in $DEPLOY_DIR_IMAGE" | 687 | echo "Error: Image '$image_filter' not found in $DEPLOY_DIR_IMAGES" |
| 415 | echo "" | 688 | echo "" |
| 416 | echo "Available images:" | 689 | echo "Available images:" |
| 417 | for oci_dir in "$DEPLOY_DIR_IMAGE"/*-oci; do | 690 | for machine_dir in "$DEPLOY_DIR_IMAGES"/*/; do |
| 418 | [ -d "$oci_dir" ] || continue | 691 | [ -d "$machine_dir" ] || continue |
| 419 | [ -f "$oci_dir/index.json" ] || continue | 692 | for oci_dir in "$machine_dir"*-oci; do |
| 420 | n=$(basename "$oci_dir" | sed 's/-latest-oci$//' | sed 's/-oci$//' | sed 's/-qemux86-64//' | sed 's/-qemuarm64//' | sed 's/\\.rootfs-[0-9]*//') | 693 | [ -d "$oci_dir" ] || continue |
| 421 | echo " $n" | 694 | [ -f "$oci_dir/index.json" ] || continue |
| 422 | done | 695 | local arch=$(get_oci_arch "$oci_dir") |
| 696 | n=$(basename "$oci_dir" | sed 's/-latest-oci$//' | sed 's/-oci$//' | sed 's/-qemux86-64//' | sed 's/-qemuarm64//' | sed 's/\\.rootfs-[0-9]*//') | ||
| 697 | echo " $n ($arch)" | ||
| 698 | done | ||
| 699 | done | sort -u | ||
| 423 | return 1 | 700 | return 1 |
| 424 | fi | 701 | fi |
| 425 | 702 | ||
| @@ -697,11 +974,23 @@ cmd_help() {{ | |||
| 697 | echo " latest The 'latest' tag" | 974 | echo " latest The 'latest' tag" |
| 698 | echo " arch Append architecture suffix to other tags" | 975 | echo " arch Append architecture suffix to other tags" |
| 699 | echo "" | 976 | echo "" |
| 977 | echo "Multi-architecture support:" | ||
| 978 | echo " Push scans all machine directories under DEPLOY_DIR_IMAGES and creates" | ||
| 979 | echo " manifest lists containing all architectures found for each container." | ||
| 980 | echo "" | ||
| 981 | echo " Workflow:" | ||
| 982 | echo " MACHINE=qemuarm64 bitbake myapp" | ||
| 983 | echo " MACHINE=qemux86-64 bitbake myapp" | ||
| 984 | echo " $0 push # Scans all machines, creates manifest lists" | ||
| 985 | echo "" | ||
| 986 | echo " Result: myapp:latest is a manifest list with both arm64 and amd64" | ||
| 987 | echo "" | ||
| 700 | echo "Examples:" | 988 | echo "Examples:" |
| 701 | echo " $0 start" | 989 | echo " $0 start" |
| 702 | echo " $0 push # Push all, default strategy" | 990 | echo " $0 push # Push all from all machines" |
| 703 | echo " $0 push container-base # Push one, default strategy" | 991 | echo " $0 push container-base # Push by name (all archs found)" |
| 704 | echo " $0 push container-base --tag v1.0.0 # Explicit tag (one image)" | 992 | echo " $0 push /path/to/container-base-latest-oci # Push by path (single OCI dir)" |
| 993 | echo " $0 push container-base --tag v1.0.0 # Explicit tag" | ||
| 705 | echo " $0 push container-base -t latest -t v1.0.0 # Multiple explicit tags" | 994 | echo " $0 push container-base -t latest -t v1.0.0 # Multiple explicit tags" |
| 706 | echo " $0 push --strategy 'sha branch latest' # All images, strategy" | 995 | echo " $0 push --strategy 'sha branch latest' # All images, strategy" |
| 707 | echo " $0 push --strategy semver --version 1.2.3 # All images, SemVer" | 996 | echo " $0 push --strategy semver --version 1.2.3 # All images, SemVer" |
| @@ -712,6 +1001,8 @@ cmd_help() {{ | |||
| 712 | echo " $0 tags container-base" | 1001 | echo " $0 tags container-base" |
| 713 | echo "" | 1002 | echo "" |
| 714 | echo "Environment variables:" | 1003 | echo "Environment variables:" |
| 1004 | echo " DEPLOY_DIR_IMAGES Override parent of deploy dirs (scans */)" | ||
| 1005 | echo " DEPLOY_DIR_IMAGE Override single machine deploy dir" | ||
| 715 | echo " CONTAINER_REGISTRY_TAG_STRATEGY Override default tag strategy" | 1006 | echo " CONTAINER_REGISTRY_TAG_STRATEGY Override default tag strategy" |
| 716 | echo " IMAGE_VERSION Version for semver/version strategies" | 1007 | echo " IMAGE_VERSION Version for semver/version strategies" |
| 717 | echo " TARGET_ARCH Architecture for arch strategy" | 1008 | echo " TARGET_ARCH Architecture for arch strategy" |
| @@ -722,7 +1013,7 @@ cmd_help() {{ | |||
| 722 | echo " Tag strategy: $DEFAULT_TAG_STRATEGY" | 1013 | echo " Tag strategy: $DEFAULT_TAG_STRATEGY" |
| 723 | echo " Target arch: $DEFAULT_TARGET_ARCH" | 1014 | echo " Target arch: $DEFAULT_TARGET_ARCH" |
| 724 | echo " Storage: $REGISTRY_STORAGE" | 1015 | echo " Storage: $REGISTRY_STORAGE" |
| 725 | echo " Deploy images: $DEPLOY_DIR_IMAGE" | 1016 | echo " Deploy dirs: $DEPLOY_DIR_IMAGES/*/" |
| 726 | }} | 1017 | }} |
| 727 | 1018 | ||
| 728 | case "${{1:-help}}" in | 1019 | case "${{1:-help}}" in |
diff --git a/recipes-containers/container-registry/docker-registry-config.bb b/recipes-containers/container-registry/docker-registry-config.bb index cbe2e13f..e03cd3eb 100644 --- a/recipes-containers/container-registry/docker-registry-config.bb +++ b/recipes-containers/container-registry/docker-registry-config.bb | |||
| @@ -46,22 +46,38 @@ LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/MIT;md5=0835ade698e0bcf8506ecda | |||
| 46 | 46 | ||
| 47 | # Space-separated list of insecure registries | 47 | # Space-separated list of insecure registries |
| 48 | # Example: "10.0.2.2:5000 myregistry.local:5000" | 48 | # Example: "10.0.2.2:5000 myregistry.local:5000" |
| 49 | # Can also use runtime-agnostic CONTAINER_REGISTRY_URL + CONTAINER_REGISTRY_INSECURE | ||
| 49 | DOCKER_REGISTRY_INSECURE ?= "" | 50 | DOCKER_REGISTRY_INSECURE ?= "" |
| 51 | CONTAINER_REGISTRY_URL ?= "" | ||
| 52 | CONTAINER_REGISTRY_INSECURE ?= "" | ||
| 53 | |||
| 54 | def get_insecure_registries(d): | ||
| 55 | """Get insecure registries from either Docker-specific or generic config""" | ||
| 56 | # Prefer explicit DOCKER_REGISTRY_INSECURE if set | ||
| 57 | docker_insecure = d.getVar('DOCKER_REGISTRY_INSECURE') or "" | ||
| 58 | if docker_insecure.strip(): | ||
| 59 | return docker_insecure.strip() | ||
| 60 | # Fall back to CONTAINER_REGISTRY_URL if marked insecure | ||
| 61 | registry_url = d.getVar('CONTAINER_REGISTRY_URL') or "" | ||
| 62 | is_insecure = d.getVar('CONTAINER_REGISTRY_INSECURE') or "" | ||
| 63 | if registry_url.strip() and is_insecure in ['1', 'true', 'yes']: | ||
| 64 | return registry_url.strip() | ||
| 65 | return "" | ||
| 50 | 66 | ||
| 51 | inherit allarch | 67 | inherit allarch |
| 52 | 68 | ||
| 53 | # Skip recipe entirely if not configured | 69 | # Skip recipe entirely if not configured |
| 54 | python() { | 70 | python() { |
| 55 | registries = d.getVar('DOCKER_REGISTRY_INSECURE') | 71 | registries = get_insecure_registries(d) |
| 56 | if not registries or not registries.strip(): | 72 | if not registries: |
| 57 | raise bb.parse.SkipRecipe("DOCKER_REGISTRY_INSECURE not set - recipe is opt-in only") | 73 | raise bb.parse.SkipRecipe("No insecure registry configured - recipe is opt-in only") |
| 58 | } | 74 | } |
| 59 | 75 | ||
| 60 | python do_install() { | 76 | python do_install() { |
| 61 | import os | 77 | import os |
| 62 | import json | 78 | import json |
| 63 | 79 | ||
| 64 | registries = d.getVar('DOCKER_REGISTRY_INSECURE').split() | 80 | registries = get_insecure_registries(d).split() |
| 65 | 81 | ||
| 66 | dest = d.getVar('D') | 82 | dest = d.getVar('D') |
| 67 | confdir = os.path.join(dest, d.getVar('sysconfdir').lstrip('/'), 'docker') | 83 | confdir = os.path.join(dest, d.getVar('sysconfdir').lstrip('/'), 'docker') |
