diff options
| author | Bruce Ashfield <bruce.ashfield@gmail.com> | 2026-01-14 04:45:53 +0000 |
|---|---|---|
| committer | Bruce Ashfield <bruce.ashfield@gmail.com> | 2026-02-09 03:32:52 +0000 |
| commit | b02a9d60012bc4ec556238738ffe5285001e0c59 (patch) | |
| tree | d564422985e500bc860d2a344270d533da57c8a6 /classes/image-oci.bbclass | |
| parent | 68320b2c0a6751bf54ae9376d6e1e1dab30c0376 (diff) | |
| download | meta-virtualization-b02a9d60012bc4ec556238738ffe5285001e0c59.tar.gz | |
image-oci: add multi-layer OCI support and CMD default
Add support for multi-layer OCI images, enabling base + app layer builds:
Multi-layer support:
- Add OCI_BASE_IMAGE variable to specify base layer (recipe name or path)
- Add OCI_BASE_IMAGE_TAG for selecting base image tag (default: latest)
- Resolve base image type (recipe/path/remote) at parse time
- Copy base OCI layout before adding new layer via umoci repack
- Fix merged-usr whiteout ordering issue for non-merged-usr base images
(replaces problematic whiteouts with filtered entries to avoid Docker
pull failures when layering merged-usr on traditional layout)
CMD/ENTRYPOINT behavior change:
- Add OCI_IMAGE_CMD variable (default: "/bin/sh")
- Change OCI_IMAGE_ENTRYPOINT default to empty string
- This makes `docker run image /bin/sh` work as expected (like Docker Hub images)
- OCI_IMAGE_ENTRYPOINT_ARGS still works for legacy compatibility
- Fix shlex.split() for proper shell quoting in CMD/ENTRYPOINT values
The multi-layer feature requires umoci backend (default). The sloci backend
only supports single-layer images and will error if OCI_BASE_IMAGE is set.
Example usage:
OCI_BASE_IMAGE = "container-base"
IMAGE_INSTALL = "myapp"
OCI_IMAGE_CMD = "/usr/bin/myapp"
Signed-off-by: Bruce Ashfield <bruce.ashfield@gmail.com>
Diffstat (limited to 'classes/image-oci.bbclass')
| -rw-r--r-- | classes/image-oci.bbclass | 105 |
1 files changed, 104 insertions, 1 deletions
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} |
