diff options
Diffstat (limited to 'classes')
| -rw-r--r-- | classes/image-oci-umoci.inc | 305 | ||||
| -rw-r--r-- | classes/image-oci.bbclass | 105 |
2 files changed, 395 insertions, 15 deletions
diff --git a/classes/image-oci-umoci.inc b/classes/image-oci-umoci.inc index 0dba347c..340f298b 100644 --- a/classes/image-oci-umoci.inc +++ b/classes/image-oci-umoci.inc | |||
| @@ -1,3 +1,187 @@ | |||
| 1 | # Fix merged-usr whiteout issues in OCI layer | ||
| 2 | # When a directory becomes a symlink, umoci creates whiteouts inside it, but | ||
| 3 | # puts them after the symlink in the tar. Docker fails because it can't create | ||
| 4 | # files inside a symlink. This function replaces individual whiteouts with | ||
| 5 | # opaque whiteouts and fixes tar ordering. | ||
| 6 | oci_fix_merged_usr_whiteouts() { | ||
| 7 | local image_dir="$1" | ||
| 8 | local tag="$2" | ||
| 9 | local needs_fix=false | ||
| 10 | |||
| 11 | # Find the manifest for this tag | ||
| 12 | local manifest_digest=$(jq -r '.manifests[] | select(.annotations["org.opencontainers.image.ref.name"] == "'"$tag"'") | .digest' "$image_dir/index.json" | sed 's/sha256://') | ||
| 13 | if [ -z "$manifest_digest" ]; then | ||
| 14 | bbdebug 1 "OCI fix: Could not find manifest for tag $tag" | ||
| 15 | return 0 | ||
| 16 | fi | ||
| 17 | |||
| 18 | # Get the last layer (newest, the one we just added) | ||
| 19 | local layer_digest=$(jq -r '.layers[-1].digest' "$image_dir/blobs/sha256/$manifest_digest" | sed 's/sha256://') | ||
| 20 | if [ -z "$layer_digest" ]; then | ||
| 21 | bbdebug 1 "OCI fix: Could not find layer digest" | ||
| 22 | return 0 | ||
| 23 | fi | ||
| 24 | |||
| 25 | local layer_blob="$image_dir/blobs/sha256/$layer_digest" | ||
| 26 | if [ ! -f "$layer_blob" ]; then | ||
| 27 | bbdebug 1 "OCI fix: Layer blob not found: $layer_blob" | ||
| 28 | return 0 | ||
| 29 | fi | ||
| 30 | |||
| 31 | # Convert to absolute path before we cd elsewhere | ||
| 32 | layer_blob=$(readlink -f "$layer_blob") | ||
| 33 | image_dir=$(readlink -f "$image_dir") | ||
| 34 | |||
| 35 | # Get tar listing with details to identify symlinks and whiteouts | ||
| 36 | local layer_listing=$(tar -tvzf "$layer_blob" 2>/dev/null || true) | ||
| 37 | local layer_files=$(tar -tzf "$layer_blob" 2>/dev/null || true) | ||
| 38 | |||
| 39 | # Find directories that are symlinks but have whiteouts listed inside | ||
| 40 | # Include merged-usr dirs (bin, sbin, lib) and var/* symlinks | ||
| 41 | local dirs_to_fix="" | ||
| 42 | for dir in bin sbin lib lib64 var/lock var/log var/run var/tmp; do | ||
| 43 | # Check if $dir is a symlink in the tar (line starts with 'l') | ||
| 44 | if echo "$layer_listing" | grep -q "^l.* ${dir} -> "; then | ||
| 45 | # Check if there are whiteouts "inside" it | ||
| 46 | if echo "$layer_files" | grep -q "^${dir}/\.wh\."; then | ||
| 47 | bbnote "OCI fix: Found problematic whiteout pattern in $dir" | ||
| 48 | dirs_to_fix="$dirs_to_fix $dir" | ||
| 49 | needs_fix=true | ||
| 50 | fi | ||
| 51 | fi | ||
| 52 | done | ||
| 53 | |||
| 54 | if [ "$needs_fix" != "true" ]; then | ||
| 55 | bbdebug 1 "OCI fix: No merged-usr whiteout issues detected" | ||
| 56 | return 0 | ||
| 57 | fi | ||
| 58 | |||
| 59 | bbnote "OCI fix: Fixing merged-usr whiteout ordering in layer" | ||
| 60 | bbnote "OCI fix: Directories to fix:$dirs_to_fix" | ||
| 61 | |||
| 62 | # Save current directory | ||
| 63 | local orig_dir=$(pwd) | ||
| 64 | |||
| 65 | # Create temp directory for fix | ||
| 66 | local fix_dir=$(mktemp -d) | ||
| 67 | local fixed_tar="$fix_dir/fixed-layer.tar" | ||
| 68 | |||
| 69 | cd "$fix_dir" | ||
| 70 | |||
| 71 | # Strategy: Simply remove the problematic whiteouts | ||
| 72 | # The symlink itself will hide the base directory contents. | ||
| 73 | # We don't need opaque whiteouts - they would hide ALL base content. | ||
| 74 | |||
| 75 | # Build exclude pattern - whiteouts in symlinked dirs | ||
| 76 | local exclude_pattern="" | ||
| 77 | for dir in $dirs_to_fix; do | ||
| 78 | exclude_pattern="${exclude_pattern}|^${dir}/\\.wh\\." | ||
| 79 | done | ||
| 80 | exclude_pattern="${exclude_pattern#|}" # Remove leading | | ||
| 81 | |||
| 82 | # Use Python to filter the tar - just remove problematic whiteouts | ||
| 83 | python3 << PYEOF | ||
| 84 | import tarfile | ||
| 85 | import gzip | ||
| 86 | import re | ||
| 87 | |||
| 88 | src_blob = "$layer_blob" | ||
| 89 | dst_tar = "$fixed_tar" | ||
| 90 | exclude_re = re.compile(r'$exclude_pattern') | ||
| 91 | |||
| 92 | removed_count = 0 | ||
| 93 | |||
| 94 | # Read source tar and filter out problematic whiteouts | ||
| 95 | with gzip.open(src_blob, 'rb') as gz: | ||
| 96 | with tarfile.open(fileobj=gz, mode='r:') as src: | ||
| 97 | with tarfile.open(dst_tar, 'w') as dst: | ||
| 98 | for member in src.getmembers(): | ||
| 99 | # Skip whiteouts in dirs that became symlinks | ||
| 100 | if exclude_re.match(member.name): | ||
| 101 | removed_count += 1 | ||
| 102 | continue | ||
| 103 | |||
| 104 | # Copy the member | ||
| 105 | if member.isfile(): | ||
| 106 | dst.addfile(member, src.extractfile(member)) | ||
| 107 | else: | ||
| 108 | dst.addfile(member) | ||
| 109 | |||
| 110 | print(f"Removed {removed_count} problematic whiteouts from layer") | ||
| 111 | PYEOF | ||
| 112 | |||
| 113 | # Calculate diff_id (uncompressed digest) before compressing | ||
| 114 | local new_diff_id=$(sha256sum "$fixed_tar" | cut -d' ' -f1) | ||
| 115 | local old_diff_id=$(gunzip -c "$layer_blob" | sha256sum | cut -d' ' -f1) | ||
| 116 | |||
| 117 | # Compress the fixed tar | ||
| 118 | gzip -n -f "$fixed_tar" | ||
| 119 | local fixed_blob="$fixed_tar.gz" | ||
| 120 | |||
| 121 | # Calculate new digest (compressed) | ||
| 122 | local new_digest=$(sha256sum "$fixed_blob" | cut -d' ' -f1) | ||
| 123 | local new_size=$(stat -c%s "$fixed_blob") | ||
| 124 | |||
| 125 | bbnote "OCI fix: New layer digest: sha256:$new_digest (was sha256:$layer_digest)" | ||
| 126 | bbnote "OCI fix: New diff_id: sha256:$new_diff_id (was sha256:$old_diff_id)" | ||
| 127 | |||
| 128 | # Replace the blob | ||
| 129 | cp "$fixed_blob" "$image_dir/blobs/sha256/$new_digest" | ||
| 130 | rm -f "$image_dir/blobs/sha256/$layer_digest" | ||
| 131 | |||
| 132 | # Update manifest with new layer digest and size | ||
| 133 | local manifest_file="$image_dir/blobs/sha256/$manifest_digest" | ||
| 134 | jq --arg old "sha256:$layer_digest" --arg new "sha256:$new_digest" --argjson size "$new_size" \ | ||
| 135 | '(.layers[] | select(.digest == $old)) |= (.digest = $new | .size = $size)' \ | ||
| 136 | "$manifest_file" > "$manifest_file.new" | ||
| 137 | mv "$manifest_file.new" "$manifest_file" | ||
| 138 | |||
| 139 | # Get config digest from manifest and update diff_ids in config | ||
| 140 | local config_digest=$(jq -r '.config.digest' "$manifest_file" | sed 's/sha256://') | ||
| 141 | local config_file="$image_dir/blobs/sha256/$config_digest" | ||
| 142 | |||
| 143 | bbnote "OCI fix: Updating config $config_digest" | ||
| 144 | |||
| 145 | # Update the last diff_id in the config (our layer) | ||
| 146 | # Use direct index replacement since we know which layer we fixed | ||
| 147 | jq --arg new "sha256:$new_diff_id" \ | ||
| 148 | '.rootfs.diff_ids[-1] = $new' \ | ||
| 149 | "$config_file" > "$config_file.new" | ||
| 150 | mv "$config_file.new" "$config_file" | ||
| 151 | |||
| 152 | # Recalculate config digest | ||
| 153 | local new_config_digest=$(sha256sum "$config_file" | cut -d' ' -f1) | ||
| 154 | local new_config_size=$(stat -c%s "$config_file") | ||
| 155 | |||
| 156 | if [ "$new_config_digest" != "$config_digest" ]; then | ||
| 157 | mv "$config_file" "$image_dir/blobs/sha256/$new_config_digest" | ||
| 158 | # Update manifest with new config digest | ||
| 159 | jq --arg old "sha256:$config_digest" --arg new "sha256:$new_config_digest" --argjson size "$new_config_size" \ | ||
| 160 | '.config |= (if .digest == $old then .digest = $new | .size = $size else . end)' \ | ||
| 161 | "$manifest_file" > "$manifest_file.new" | ||
| 162 | mv "$manifest_file.new" "$manifest_file" | ||
| 163 | fi | ||
| 164 | |||
| 165 | # Recalculate manifest digest | ||
| 166 | local new_manifest_digest=$(sha256sum "$manifest_file" | cut -d' ' -f1) | ||
| 167 | local new_manifest_size=$(stat -c%s "$manifest_file") | ||
| 168 | |||
| 169 | if [ "$new_manifest_digest" != "$manifest_digest" ]; then | ||
| 170 | mv "$manifest_file" "$image_dir/blobs/sha256/$new_manifest_digest" | ||
| 171 | # Update index.json | ||
| 172 | jq --arg old "sha256:$manifest_digest" --arg new "sha256:$new_manifest_digest" --argjson size "$new_manifest_size" \ | ||
| 173 | '(.manifests[] | select(.digest == $old)) |= (.digest = $new | .size = $size)' \ | ||
| 174 | "$image_dir/index.json" > "$image_dir/index.json.new" | ||
| 175 | mv "$image_dir/index.json.new" "$image_dir/index.json" | ||
| 176 | fi | ||
| 177 | |||
| 178 | # Restore original directory and cleanup | ||
| 179 | cd "$orig_dir" | ||
| 180 | rm -rf "$fix_dir" | ||
| 181 | |||
| 182 | bbnote "OCI fix: Layer whiteout fix complete" | ||
| 183 | } | ||
| 184 | |||
| 1 | IMAGE_CMD:oci() { | 185 | IMAGE_CMD:oci() { |
| 2 | umoci_options="" | 186 | umoci_options="" |
| 3 | 187 | ||
| @@ -69,22 +253,97 @@ IMAGE_CMD:oci() { | |||
| 69 | OCI_IMAGE_TAG="initial-tag" | 253 | OCI_IMAGE_TAG="initial-tag" |
| 70 | fi | 254 | fi |
| 71 | 255 | ||
| 72 | if [ -n "$new_image" ]; then | 256 | # ======================================================================== |
| 73 | bbdebug 1 "OCI: umoci init --layout $image_name" | 257 | # PHASE 1: Initialize OCI layout (from scratch or from base image) |
| 74 | umoci init --layout $image_name | 258 | # ======================================================================== |
| 75 | umoci new --image $image_name:${OCI_IMAGE_TAG} | 259 | if [ -n "${_OCI_BASE_RECIPE}" ] || [ -n "${_OCI_BASE_PATH}" ]; then |
| 76 | umoci unpack --rootless --image $image_name:${OCI_IMAGE_TAG} $image_bundle_name | 260 | # Using a base image |
| 261 | base_oci_dir="" | ||
| 262 | base_tag="${OCI_BASE_IMAGE_TAG}" | ||
| 263 | |||
| 264 | if [ -n "${_OCI_BASE_RECIPE}" ]; then | ||
| 265 | # Use exact symlink naming: ${recipe}-${tag}-oci | ||
| 266 | base_oci_dir="${DEPLOY_DIR_IMAGE}/${_OCI_BASE_RECIPE}-${base_tag}-oci" | ||
| 267 | |||
| 268 | if [ ! -d "$base_oci_dir" ] || [ ! -f "$base_oci_dir/index.json" ]; then | ||
| 269 | bbfatal "OCI: Base image '${_OCI_BASE_RECIPE}' not found at expected path: $base_oci_dir" | ||
| 270 | fi | ||
| 271 | elif [ -n "${_OCI_BASE_PATH}" ]; then | ||
| 272 | base_oci_dir="${_OCI_BASE_PATH}" | ||
| 273 | if [ ! -d "$base_oci_dir" ] || [ ! -f "$base_oci_dir/index.json" ]; then | ||
| 274 | bbfatal "OCI: Base image path not valid: $base_oci_dir" | ||
| 275 | fi | ||
| 276 | fi | ||
| 277 | |||
| 278 | # Resolve symlinks to get actual directory | ||
| 279 | base_oci_dir=$(readlink -f "$base_oci_dir") | ||
| 280 | bbnote "OCI: Using base image from: $base_oci_dir" | ||
| 281 | |||
| 282 | # Copy base image layout to our image directory (-L to follow symlinks) | ||
| 283 | cp -rL "$base_oci_dir" "$image_name" | ||
| 284 | |||
| 285 | # Count existing layers for logging (simplified) | ||
| 286 | base_layers=$(ls "$image_name/blobs/sha256/" 2>/dev/null | wc -l) | ||
| 287 | bbnote "OCI: Base image has approximately $base_layers blob(s)" | ||
| 288 | |||
| 289 | # Unpack base image for modification | ||
| 290 | umoci unpack --rootless --image "$image_name:$base_tag" "$image_bundle_name" | ||
| 291 | elif [ -n "$new_image" ]; then | ||
| 292 | # No base image - create empty OCI layout | ||
| 293 | bbdebug 1 "OCI: umoci init --layout $image_name" | ||
| 294 | umoci init --layout $image_name | ||
| 295 | umoci new --image $image_name:${OCI_IMAGE_TAG} | ||
| 296 | umoci unpack --rootless --image $image_name:${OCI_IMAGE_TAG} $image_bundle_name | ||
| 77 | else | 297 | else |
| 78 | # todo: create a different tag, after checking if the passed one exists | 298 | # todo: create a different tag, after checking if the passed one exists |
| 79 | true | 299 | true |
| 80 | fi | 300 | fi |
| 81 | 301 | ||
| 302 | # ======================================================================== | ||
| 303 | # PHASE 2: Add content layer(s) | ||
| 304 | # ======================================================================== | ||
| 82 | bbdebug 1 "OCI: populating rootfs" | 305 | bbdebug 1 "OCI: populating rootfs" |
| 83 | bbdebug 1 "OCI: cp -r ${IMAGE_ROOTFS}/* $image_bundle_name/rootfs/" | ||
| 84 | cp -r -a --no-preserve=ownership ${IMAGE_ROOTFS}/* $image_bundle_name/rootfs | ||
| 85 | 306 | ||
| 86 | bbdebug 1 "OCI: umoci repack --image $image_name:${OCI_IMAGE_TAG} $image_bundle_name" | 307 | # Use rsync for robust merging when base image exists (handles symlink vs dir conflicts) |
| 87 | umoci repack --image $image_name:${OCI_IMAGE_TAG} $image_bundle_name | 308 | # For no-base builds, cp is sufficient and faster |
| 309 | # Note: When source has symlinks replacing dest directories, we first remove conflicting dirs | ||
| 310 | if [ -n "${_OCI_BASE_RECIPE}" ] || [ -n "${_OCI_BASE_PATH}" ]; then | ||
| 311 | # Handle Yocto's merged-usr symlinks (/bin -> /usr/bin) and /var symlinks | ||
| 312 | # replacing Alpine's or other base image directories | ||
| 313 | for p in bin lib lib64 sbin var/lock var/log var/tmp; do | ||
| 314 | src="${IMAGE_ROOTFS}/$p" | ||
| 315 | dst="$image_bundle_name/rootfs/$p" | ||
| 316 | if [ -L "$src" ] && [ -d "$dst" ] && [ ! -L "$dst" ]; then | ||
| 317 | bbdebug 1 "OCI: removing directory $dst to replace with symlink" | ||
| 318 | rm -rf "$dst" | ||
| 319 | fi | ||
| 320 | done | ||
| 321 | bbdebug 1 "OCI: rsync -a --no-owner --no-group ${IMAGE_ROOTFS}/ $image_bundle_name/rootfs/" | ||
| 322 | rsync -a --no-owner --no-group ${IMAGE_ROOTFS}/ $image_bundle_name/rootfs/ | ||
| 323 | else | ||
| 324 | bbdebug 1 "OCI: cp -r ${IMAGE_ROOTFS}/* $image_bundle_name/rootfs/" | ||
| 325 | cp -r -a --no-preserve=ownership ${IMAGE_ROOTFS}/* $image_bundle_name/rootfs | ||
| 326 | fi | ||
| 327 | |||
| 328 | # Determine which tag to use for repack | ||
| 329 | repack_tag="${OCI_IMAGE_TAG}" | ||
| 330 | if [ -n "${_OCI_BASE_RECIPE}" ] || [ -n "${_OCI_BASE_PATH}" ]; then | ||
| 331 | repack_tag="${OCI_BASE_IMAGE_TAG}" | ||
| 332 | fi | ||
| 333 | |||
| 334 | bbdebug 1 "OCI: umoci repack --image $image_name:$repack_tag $image_bundle_name" | ||
| 335 | umoci repack --image $image_name:$repack_tag $image_bundle_name | ||
| 336 | |||
| 337 | # If we used a base image with different tag, re-tag to our target tag | ||
| 338 | if [ -n "${_OCI_BASE_RECIPE}" ] || [ -n "${_OCI_BASE_PATH}" ]; then | ||
| 339 | if [ "$repack_tag" != "${OCI_IMAGE_TAG}" ]; then | ||
| 340 | umoci tag --image "$image_name:$repack_tag" "${OCI_IMAGE_TAG}" | ||
| 341 | fi | ||
| 342 | |||
| 343 | # Log final layer count (simplified - count blobs minus config/manifest) | ||
| 344 | final_blobs=$(ls "$image_name/blobs/sha256/" 2>/dev/null | wc -l) | ||
| 345 | bbnote "OCI: Final image has approximately $final_blobs blob(s)" | ||
| 346 | fi | ||
| 88 | 347 | ||
| 89 | bbdebug 1 "OCI: configuring image" | 348 | bbdebug 1 "OCI: configuring image" |
| 90 | if [ -n "${OCI_IMAGE_LABELS}" ]; then | 349 | if [ -n "${OCI_IMAGE_LABELS}" ]; then |
| @@ -137,13 +396,31 @@ IMAGE_CMD:oci() { | |||
| 137 | bbnote "OCI: image subarch is set to: ${OCI_IMAGE_SUBARCH}, but umoci does not" | 396 | bbnote "OCI: image subarch is set to: ${OCI_IMAGE_SUBARCH}, but umoci does not" |
| 138 | bbnote " expose variants. use sloci instead if this is important" | 397 | bbnote " expose variants. use sloci instead if this is important" |
| 139 | fi | 398 | fi |
| 140 | umoci config --image $image_name:${OCI_IMAGE_TAG} \ | 399 | # Set entrypoint if specified (for wrapper script patterns) |
| 141 | ${@" ".join("--config.entrypoint %s" % s for s in d.getVar("OCI_IMAGE_ENTRYPOINT").split())} | 400 | if [ -n "${OCI_IMAGE_ENTRYPOINT}" ]; then |
| 401 | umoci config --image $image_name:${OCI_IMAGE_TAG} \ | ||
| 402 | ${@" ".join("--config.entrypoint '%s'" % s for s in __import__('shlex').split(d.getVar("OCI_IMAGE_ENTRYPOINT")))} | ||
| 403 | fi | ||
| 404 | # Set CMD: use OCI_IMAGE_ENTRYPOINT_ARGS if set (legacy), otherwise OCI_IMAGE_CMD | ||
| 142 | if [ -n "${OCI_IMAGE_ENTRYPOINT_ARGS}" ]; then | 405 | if [ -n "${OCI_IMAGE_ENTRYPOINT_ARGS}" ]; then |
| 143 | umoci config --image $image_name:${OCI_IMAGE_TAG} ${@" ".join("--config.cmd %s" % s for s in d.getVar("OCI_IMAGE_ENTRYPOINT_ARGS").split())} | 406 | umoci config --image $image_name:${OCI_IMAGE_TAG} ${@" ".join("--config.cmd '%s'" % s for s in __import__('shlex').split(d.getVar("OCI_IMAGE_ENTRYPOINT_ARGS")))} |
| 407 | elif [ -n "${OCI_IMAGE_CMD}" ]; then | ||
| 408 | umoci config --image $image_name:${OCI_IMAGE_TAG} ${@" ".join("--config.cmd '%s'" % s for s in __import__('shlex').split(d.getVar("OCI_IMAGE_CMD")))} | ||
| 144 | fi | 409 | fi |
| 145 | umoci config --image $image_name:${OCI_IMAGE_TAG} --author ${OCI_IMAGE_AUTHOR_EMAIL} | 410 | umoci config --image $image_name:${OCI_IMAGE_TAG} --author ${OCI_IMAGE_AUTHOR_EMAIL} |
| 146 | 411 | ||
| 412 | # ======================================================================== | ||
| 413 | # PHASE 3: Fix merged-usr whiteout issues for non-merged-usr base images | ||
| 414 | # ======================================================================== | ||
| 415 | # When layering merged-usr (symlinks) on traditional layout (directories), | ||
| 416 | # umoci creates whiteouts like bin/.wh.file but puts them AFTER the bin symlink | ||
| 417 | # in the tar. Docker can't create files inside a symlink, causing pull failures. | ||
| 418 | # Fix: Replace individual whiteouts with opaque whiteouts, reorder tar entries. | ||
| 419 | # NOTE: Must run AFTER all umoci config commands since they create new config blobs. | ||
| 420 | if [ -n "${_OCI_BASE_RECIPE}" ] || [ -n "${_OCI_BASE_PATH}" ]; then | ||
| 421 | oci_fix_merged_usr_whiteouts "$image_name" "${OCI_IMAGE_TAG}" | ||
| 422 | fi | ||
| 423 | |||
| 147 | # OCI_IMAGE_TAG may contain ":", but these are not allowed in OCI file | 424 | # OCI_IMAGE_TAG may contain ":", but these are not allowed in OCI file |
| 148 | # names so replace them | 425 | # names so replace them |
| 149 | image_tag="${@d.getVar("OCI_IMAGE_TAG").replace(":", "_")}" | 426 | image_tag="${@d.getVar("OCI_IMAGE_TAG").replace(":", "_")}" |
diff --git a/classes/image-oci.bbclass b/classes/image-oci.bbclass index 70f32bf1..6f8011ca 100644 --- a/classes/image-oci.bbclass +++ b/classes/image-oci.bbclass | |||
| @@ -42,6 +42,8 @@ IMAGE_TYPEDEP:oci = "container tar.bz2" | |||
| 42 | # OCI_IMAGE_BACKEND ?= "sloci-image" | 42 | # OCI_IMAGE_BACKEND ?= "sloci-image" |
| 43 | OCI_IMAGE_BACKEND ?= "umoci" | 43 | OCI_IMAGE_BACKEND ?= "umoci" |
| 44 | do_image_oci[depends] += "${OCI_IMAGE_BACKEND}-native:do_populate_sysroot" | 44 | do_image_oci[depends] += "${OCI_IMAGE_BACKEND}-native:do_populate_sysroot" |
| 45 | # jq-native is needed for the merged-usr whiteout fix | ||
| 46 | do_image_oci[depends] += "jq-native:do_populate_sysroot" | ||
| 45 | 47 | ||
| 46 | # | 48 | # |
| 47 | # image type configuration block | 49 | # image type configuration block |
| @@ -55,8 +57,12 @@ OCI_IMAGE_RUNTIME_UID ?= "" | |||
| 55 | OCI_IMAGE_ARCH ?= "${@oe.go.map_arch(d.getVar('TARGET_ARCH'))}" | 57 | OCI_IMAGE_ARCH ?= "${@oe.go.map_arch(d.getVar('TARGET_ARCH'))}" |
| 56 | OCI_IMAGE_SUBARCH ?= "${@oci_map_subarch(d.getVar('TARGET_ARCH'), d.getVar('TUNE_FEATURES'), d)}" | 58 | OCI_IMAGE_SUBARCH ?= "${@oci_map_subarch(d.getVar('TARGET_ARCH'), d.getVar('TUNE_FEATURES'), d)}" |
| 57 | 59 | ||
| 58 | OCI_IMAGE_ENTRYPOINT ?= "sh" | 60 | # OCI_IMAGE_ENTRYPOINT: If set, this command always runs (args appended). |
| 61 | # OCI_IMAGE_CMD: Default command (replaced when user passes arguments). | ||
| 62 | # Most base images use CMD only for flexibility. Use ENTRYPOINT for wrapper scripts. | ||
| 63 | OCI_IMAGE_ENTRYPOINT ?= "" | ||
| 59 | OCI_IMAGE_ENTRYPOINT_ARGS ?= "" | 64 | OCI_IMAGE_ENTRYPOINT_ARGS ?= "" |
| 65 | OCI_IMAGE_CMD ?= "/bin/sh" | ||
| 60 | OCI_IMAGE_WORKINGDIR ?= "" | 66 | OCI_IMAGE_WORKINGDIR ?= "" |
| 61 | OCI_IMAGE_STOPSIGNAL ?= "" | 67 | OCI_IMAGE_STOPSIGNAL ?= "" |
| 62 | 68 | ||
| @@ -112,6 +118,27 @@ OCI_IMAGE_BUILD_DATE ?= "" | |||
| 112 | # Enable/disable auto-detection of git metadata (set to "0" to disable) | 118 | # Enable/disable auto-detection of git metadata (set to "0" to disable) |
| 113 | OCI_IMAGE_AUTO_LABELS ?= "1" | 119 | OCI_IMAGE_AUTO_LABELS ?= "1" |
| 114 | 120 | ||
| 121 | # ============================================================================= | ||
| 122 | # Multi-Layer OCI Support | ||
| 123 | # ============================================================================= | ||
| 124 | # | ||
| 125 | # OCI_BASE_IMAGE: Base image to build on top of | ||
| 126 | # - Recipe name: "container-base" (uses local recipe's OCI output) | ||
| 127 | # - Path: "/path/to/oci-dir" (uses existing OCI layout) | ||
| 128 | # - Registry URL: "docker.io/library/alpine:3.19" (fetches external image) | ||
| 129 | # | ||
| 130 | # OCI_LAYER_MODE: How to create layers | ||
| 131 | # - "single" (default): Single layer with complete rootfs (backward compatible) | ||
| 132 | # - "multi": Multiple layers from OCI_LAYERS definitions | ||
| 133 | # | ||
| 134 | # When OCI_BASE_IMAGE is set: | ||
| 135 | # - Base image layers are preserved | ||
| 136 | # - New content from IMAGE_ROOTFS is added as additional layer(s) | ||
| 137 | # | ||
| 138 | OCI_BASE_IMAGE ?= "" | ||
| 139 | OCI_BASE_IMAGE_TAG ?= "latest" | ||
| 140 | OCI_LAYER_MODE ?= "single" | ||
| 141 | |||
| 115 | # whether the oci image dir should be left as a directory, or | 142 | # whether the oci image dir should be left as a directory, or |
| 116 | # bundled into a tarball. | 143 | # bundled into a tarball. |
| 117 | OCI_IMAGE_TAR_OUTPUT ?= "true" | 144 | OCI_IMAGE_TAR_OUTPUT ?= "true" |
| @@ -131,6 +158,82 @@ def oci_map_subarch(a, f, d): | |||
| 131 | return '' | 158 | return '' |
| 132 | return '' | 159 | return '' |
| 133 | 160 | ||
| 161 | # ============================================================================= | ||
| 162 | # Base Image Resolution and Dependency Setup | ||
| 163 | # ============================================================================= | ||
| 164 | |||
| 165 | def oci_resolve_base_image(d): | ||
| 166 | """Resolve OCI_BASE_IMAGE to determine its type. | ||
| 167 | |||
| 168 | Returns dict with 'type' key: | ||
| 169 | - {'type': 'recipe', 'name': 'container-base'} | ||
| 170 | - {'type': 'path', 'path': '/path/to/oci-dir'} | ||
| 171 | - {'type': 'remote', 'url': 'docker.io/library/alpine:3.19'} | ||
| 172 | - None if no base image | ||
| 173 | """ | ||
| 174 | base = d.getVar('OCI_BASE_IMAGE') or '' | ||
| 175 | if not base: | ||
| 176 | return None | ||
| 177 | |||
| 178 | # Check if it's a path (starts with /) | ||
| 179 | if base.startswith('/'): | ||
| 180 | return {'type': 'path', 'path': base} | ||
| 181 | |||
| 182 | # Check if it looks like a registry URL (contains / or has registry prefix) | ||
| 183 | if '/' in base or '.' in base.split(':')[0]: | ||
| 184 | return {'type': 'remote', 'url': base} | ||
| 185 | |||
| 186 | # Assume it's a recipe name | ||
| 187 | return {'type': 'recipe', 'name': base} | ||
| 188 | |||
| 189 | python __anonymous() { | ||
| 190 | import os | ||
| 191 | |||
| 192 | backend = d.getVar('OCI_IMAGE_BACKEND') or 'umoci' | ||
| 193 | base_image = d.getVar('OCI_BASE_IMAGE') or '' | ||
| 194 | layer_mode = d.getVar('OCI_LAYER_MODE') or 'single' | ||
| 195 | |||
| 196 | # sloci doesn't support multi-layer | ||
| 197 | if backend == 'sloci-image': | ||
| 198 | if layer_mode != 'single' or base_image: | ||
| 199 | bb.fatal("Multi-layer OCI requires umoci backend. " | ||
| 200 | "Set OCI_IMAGE_BACKEND = 'umoci' or remove OCI_BASE_IMAGE") | ||
| 201 | |||
| 202 | # Resolve base image and set up dependencies | ||
| 203 | if base_image: | ||
| 204 | resolved = oci_resolve_base_image(d) | ||
| 205 | if resolved: | ||
| 206 | if resolved['type'] == 'recipe': | ||
| 207 | # Add dependency on base recipe's OCI output | ||
| 208 | # Use do_build as it works for both image recipes and oci-fetch recipes | ||
| 209 | base_recipe = resolved['name'] | ||
| 210 | d.setVar('_OCI_BASE_RECIPE', base_recipe) | ||
| 211 | d.appendVarFlag('do_image_oci', 'depends', | ||
| 212 | f" {base_recipe}:do_build rsync-native:do_populate_sysroot") | ||
| 213 | bb.debug(1, f"OCI: Using base image from recipe: {base_recipe}") | ||
| 214 | |||
| 215 | elif resolved['type'] == 'path': | ||
| 216 | d.setVar('_OCI_BASE_PATH', resolved['path']) | ||
| 217 | d.appendVarFlag('do_image_oci', 'depends', | ||
| 218 | " rsync-native:do_populate_sysroot") | ||
| 219 | bb.debug(1, f"OCI: Using base image from path: {resolved['path']}") | ||
| 220 | |||
| 221 | elif resolved['type'] == 'remote': | ||
| 222 | # Remote URLs are not supported directly - use a container-bundle recipe | ||
| 223 | remote_url = resolved['url'] | ||
| 224 | # Create sanitized key for CONTAINER_DIGESTS varflag | ||
| 225 | sanitized_key = remote_url.replace('/', '_').replace(':', '_') | ||
| 226 | bb.fatal(f"Remote base images cannot be used directly: {remote_url}\n\n" | ||
| 227 | f"Create a container-bundle recipe to fetch the external image:\n\n" | ||
| 228 | f" # recipes-containers/oci-base-images/my-base.bb\n" | ||
| 229 | f" inherit container-bundle\n" | ||
| 230 | f" CONTAINER_BUNDLES = \"{remote_url}\"\n" | ||
| 231 | f" CONTAINER_DIGESTS[{sanitized_key}] = \"sha256:...\"\n" | ||
| 232 | f" CONTAINER_BUNDLE_DEPLOY = \"1\"\n\n" | ||
| 233 | f"Get digest with: skopeo inspect docker://{remote_url} | jq -r '.Digest'\n\n" | ||
| 234 | f"Then use: OCI_BASE_IMAGE = \"my-base\"") | ||
| 235 | } | ||
| 236 | |||
| 134 | # the IMAGE_CMD:oci comes from the .inc | 237 | # the IMAGE_CMD:oci comes from the .inc |
| 135 | OCI_IMAGE_BACKEND_INC ?= "${@"image-oci-" + "${OCI_IMAGE_BACKEND}" + ".inc"}" | 238 | OCI_IMAGE_BACKEND_INC ?= "${@"image-oci-" + "${OCI_IMAGE_BACKEND}" + ".inc"}" |
| 136 | include ${OCI_IMAGE_BACKEND_INC} | 239 | include ${OCI_IMAGE_BACKEND_INC} |
