summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorBruce Ashfield <bruce.ashfield@gmail.com>2026-01-01 17:14:05 +0000
committerBruce Ashfield <bruce.ashfield@gmail.com>2026-02-09 03:32:52 +0000
commit79d03d5350c446223c847135c7115a656adc01d9 (patch)
tree4c988222e2de61d49ce592de0d40175ee61400cd
parent0cdb55047d352ccfffcf76d242ca132315bd0659 (diff)
downloadmeta-virtualization-79d03d5350c446223c847135c7115a656adc01d9.tar.gz
container-bundle: add package-based container bundling support
This class creates installable packages that bundle pre-processed container images. When installed via IMAGE_INSTALL, containers are automatically merged into the target image's container storage. Component relationships for bundling a local container: 1. Application Recipe (builds the software) recipes-demo/myapp/myapp_1.0.bb - Compiles application binaries - Creates installable package (myapp) 2. Container Image Recipe (creates OCI image containing the app) recipes-demo/images/myapp-container.bb - inherit image image-oci - IMAGE_INSTALL = "myapp" - Produces: ${DEPLOY_DIR_IMAGE}/myapp-container-latest-oci/ 3. Bundle Recipe (packages container images for deployment) recipes-demo/bundles/my-bundle_1.0.bb - inherit container-bundle - CONTAINER_BUNDLES = "myapp-container:autostart" - Creates installable package with OCI data Flow: application recipe -> container image recipe -> bundle recipe -> IMAGE_INSTALL in host image -> container deployed on target Usage: inherit container-bundle CONTAINER_BUNDLES = "myapp-container:autostart redis-container" CONTAINER_BUNDLES format: source[:autostart-policy] - source: Container IMAGE recipe name or remote registry URL - autostart-policy: Optional (autostart, always, unless-stopped, on-failure) Features: - Auto-generates dependencies on container image recipes (do_image_complete) - Supports remote containers via skopeo (requires CONTAINER_DIGESTS) - Runtime auto-detected from CONTAINER_PROFILE (docker/podman) - Produces OCI directories and metadata for container-cross-install Signed-off-by: Bruce Ashfield <bruce.ashfield@gmail.com>
-rw-r--r--classes/container-bundle.bbclass441
-rw-r--r--recipes-demo/autostart-test/autostart-test_1.0.bb16
-rw-r--r--recipes-demo/autostart-test/files/autostart-test.sh21
-rw-r--r--recipes-demo/images/autostart-test-container.bb24
-rw-r--r--recipes-extended/container-bundles/example-container-bundle_1.0.bb68
-rw-r--r--recipes-extended/container-bundles/remote-container-bundle_1.0.bb50
6 files changed, 620 insertions, 0 deletions
diff --git a/classes/container-bundle.bbclass b/classes/container-bundle.bbclass
new file mode 100644
index 00000000..fdd241ee
--- /dev/null
+++ b/classes/container-bundle.bbclass
@@ -0,0 +1,441 @@
1# SPDX-FileCopyrightText: Copyright (C) 2025 Bruce Ashfield
2#
3# SPDX-License-Identifier: MIT
4#
5# container-bundle.bbclass
6# ===========================================================================
7# Container bundling class for creating installable container packages
8# ===========================================================================
9#
10# This class creates packages that bundle pre-processed container images.
11# When these packages are installed via IMAGE_INSTALL, the containers are
12# automatically merged into the target image's container storage.
13#
14# ===========================================================================
15# Component Relationships
16# ===========================================================================
17#
18# To bundle a local container like "myapp:autostart", three recipe types
19# work together:
20#
21# 1. Application Recipe (builds the software)
22# recipes-demo/myapp/myapp_1.0.bb
23# ├── Compiles application binaries
24# └── Creates installable package (myapp)
25#
26# 2. Container Image Recipe (creates OCI image containing the app)
27# recipes-demo/images/myapp-container.bb
28# ├── inherit image image-oci
29# ├── IMAGE_INSTALL = "myapp"
30# └── Produces: ${DEPLOY_DIR_IMAGE}/myapp-container-latest-oci/
31#
32# 3. Bundle Recipe (packages container images for deployment)
33# recipes-demo/bundles/my-bundle_1.0.bb
34# ├── inherit container-bundle
35# ├── CONTAINER_BUNDLES = "myapp-container:autostart"
36# └── Creates installable package with OCI data
37#
38# Flow diagram:
39#
40# myapp_1.0.bb myapp-container.bb
41# (application) (container image)
42# │ │
43# │ IMAGE_INSTALL="myapp" │ inherit image-oci
44# └──────────────┬────────────────┘
45# │
46# ▼
47# myapp-container-latest-oci/
48# (OCI directory in DEPLOY_DIR_IMAGE)
49# │
50# │ CONTAINER_BUNDLES="myapp-container"
51# ▼
52# my-bundle_1.0.bb ──────► my-bundle package
53# (inherits container-bundle) │
54# │ IMAGE_INSTALL="my-bundle"
55# ▼
56# container-image-host
57# (target host image)
58#
59# ===========================================================================
60# When to Use This Class vs BUNDLED_CONTAINERS
61# ===========================================================================
62#
63# There are two ways to bundle containers into a host image:
64#
65# 1. BUNDLED_CONTAINERS variable (simpler, no extra recipe needed)
66# Set in local.conf or image recipe:
67# BUNDLED_CONTAINERS = "container-base:docker myapp-container:docker:autostart"
68#
69# 2. container-bundle packages (this class)
70# Create a bundle recipe, install via IMAGE_INSTALL
71#
72# Decision guide:
73#
74# Use Case | BUNDLED_CONTAINERS | Bundle Recipe
75# --------------------------------------------|--------------------|--------------
76# Simple: containers in one host image | recommended | overkill
77# Reuse containers across multiple images | repetitive | recommended
78# Remote containers (docker.io/library/...) | not supported | required
79# Package versioning and dependencies | not supported | supported
80# Distribute pre-built container set | not supported | supported
81#
82# For most single-image use cases, BUNDLED_CONTAINERS is simpler:
83# - No bundle recipe needed
84# - Dependencies auto-generated at parse time
85# - vrunner batch-import runs once for all containers
86#
87# Use this class (container-bundle) when you need:
88# - Remote container fetching via skopeo
89# - A distributable/versioned package of containers
90# - To share the same bundle across multiple different host images
91#
92# ===========================================================================
93# Usage
94# ===========================================================================
95#
96# inherit container-bundle
97#
98# CONTAINER_BUNDLES = "\
99# myapp-container \
100# mydb-container:autostart \
101# docker.io/library/redis:7 \
102# "
103#
104# # REQUIRED for remote containers (sanitize key: replace / and : with _):
105# CONTAINER_DIGESTS[docker.io_library_redis_7] = "sha256:..."
106#
107# # To get the digest, use skopeo:
108# # skopeo inspect docker://docker.io/library/redis:7 | jq -r '.Digest'
109#
110# Variable format: source[:autostart-policy]
111# - source: Either a container image recipe name or a remote registry URL
112# * Local: "myapp-container", "container-base" (recipe names)
113# * Remote: "docker.io/library/alpine:3.19" (contains / or .)
114# - autostart-policy: Optional. autostart | always | unless-stopped | on-failure
115#
116# Runtime Selection (in order of precedence):
117# 1. CONTAINER_BUNDLE_RUNTIME in recipe (explicit override)
118# 2. CONTAINER_PROFILE distro/local.conf setting
119# 3. Default: "docker"
120#
121# Remote containers:
122# - Must have pinned digest via CONTAINER_DIGESTS
123# - A licensing warning is emitted during fetch
124# - Fetched using skopeo-native in do_fetch phase
125#
126# Local containers:
127# - Must be container IMAGE recipes (inherit image-oci)
128# - Built via dependency on recipe:do_image_complete
129# - OCI directory picked up from DEPLOY_DIR_IMAGE
130#
131# ===========================================================================
132# Integration with container-cross-install.bbclass
133# ===========================================================================
134#
135# This class creates packages that are processed by container-cross-install:
136# 1. Installs OCI directories to ${datadir}/container-bundles/${RUNTIME}/oci/
137# 2. Installs refs file to ${datadir}/container-bundles/${RUNTIME}/${PN}.refs
138# 3. Installs metadata to ${datadir}/container-bundles/${PN}.meta
139# 4. container-cross-install.bbclass imports these via vrunner at image time
140#
141# The runtime subdirectory (docker/ vs podman/) tells container-cross-install
142# which vrunner runtime to use for import.
143#
144# See also: container-cross-install.bbclass
145
146CONTAINER_BUNDLES ?= ""
147
148# Default runtime based on CONTAINER_PROFILE
149# Can be overridden in recipe with CONTAINER_BUNDLE_RUNTIME = "podman"
150def get_bundle_runtime(d):
151 """Determine container runtime from CONTAINER_PROFILE or default to docker"""
152 profile = d.getVar('CONTAINER_PROFILE') or 'docker'
153 if profile in ['podman']:
154 return 'podman'
155 # docker, containerd, k3s-*, default all use docker storage format
156 return 'docker'
157
158CONTAINER_BUNDLE_RUNTIME ?= "${@get_bundle_runtime(d)}"
159
160# Dependencies on native tools
161# vcontainer-native provides vrunner.sh
162# Blobs come from multiconfig builds (vdkr-initramfs-create, vpdmn-initramfs-create)
163DEPENDS += "qemuwrapper-cross qemu-system-native skopeo-native"
164DEPENDS += "vcontainer-native"
165
166# Determine multiconfig name for blob building based on target architecture
167def get_vruntime_multiconfig(d):
168 arch = d.getVar('TARGET_ARCH')
169 if arch == 'aarch64':
170 return 'vruntime-aarch64'
171 elif arch in ['x86_64', 'i686', 'i586']:
172 return 'vruntime-x86-64'
173 else:
174 return None
175
176# Get the MACHINE name used in the multiconfig (for deploy path)
177def get_vruntime_machine(d):
178 arch = d.getVar('TARGET_ARCH')
179 if arch == 'aarch64':
180 return 'qemuarm64'
181 elif arch in ['x86_64', 'i686', 'i586']:
182 return 'qemux86-64'
183 else:
184 return None
185
186# Map TARGET_ARCH to blob directory name (aarch64, x86_64)
187def get_blob_arch(d):
188 """Map Yocto TARGET_ARCH to blob directory name"""
189 arch = d.getVar('TARGET_ARCH')
190 blob_map = {
191 'aarch64': 'aarch64',
192 'arm': 'aarch64', # Use aarch64 blobs for 32-bit ARM too
193 'x86_64': 'x86_64',
194 'i686': 'x86_64',
195 'i586': 'x86_64',
196 }
197 return blob_map.get(arch, 'aarch64')
198
199VRUNTIME_MULTICONFIG = "${@get_vruntime_multiconfig(d)}"
200VRUNTIME_MACHINE = "${@get_vruntime_machine(d)}"
201BLOB_ARCH = "${@get_blob_arch(d)}"
202
203# Path to vrunner.sh from vdkr-native
204VRUNNER_PATH = "${STAGING_BINDIR_NATIVE}/vrunner.sh"
205
206# Blobs come from multiconfig deploy directory
207# These are built by vdkr-initramfs-create and vpdmn-initramfs-create
208VDKR_BLOB_DIR = "${TOPDIR}/tmp-${VRUNTIME_MULTICONFIG}/deploy/images/${VRUNTIME_MACHINE}/vdkr"
209VPDMN_BLOB_DIR = "${TOPDIR}/tmp-${VRUNTIME_MULTICONFIG}/deploy/images/${VRUNTIME_MACHINE}/vpdmn"
210
211def is_remote_container(source):
212 """Detect if source is a registry URL vs local recipe name.
213
214 Remote indicators: contains '/' or '.' in the base name (before first :)
215 Local: simple recipe name like "myapp" or "container-base"
216 """
217 base = source.split(':')[0] if ':' in source else source
218 return '/' in base or '.' in base
219
220python __anonymous() {
221 bundles = (d.getVar('CONTAINER_BUNDLES') or "").split()
222 if not bundles:
223 return
224
225 # Get runtime from CONTAINER_BUNDLE_RUNTIME (set based on CONTAINER_PROFILE)
226 runtime = d.getVar('CONTAINER_BUNDLE_RUNTIME') or 'docker'
227 if runtime not in ['docker', 'podman']:
228 bb.fatal(f"Invalid CONTAINER_BUNDLE_RUNTIME '{runtime}': must be 'docker' or 'podman'")
229
230 local_recipes = []
231 remote_urls = []
232 processed_bundles = []
233
234 for bundle in bundles:
235 # New format: source[:autostart-policy]
236 # For remote URLs like docker.io/library/redis:7, we need to handle
237 # the tag colon differently from the autostart colon
238 if is_remote_container(bundle):
239 # Remote: could be "docker.io/library/redis:7" or "docker.io/library/redis:7:autostart"
240 # Find the last colon that's an autostart policy
241 if bundle.endswith(':autostart') or bundle.endswith(':always') or \
242 bundle.endswith(':unless-stopped') or bundle.endswith(':on-failure') or \
243 bundle.endswith(':no'):
244 last_colon = bundle.rfind(':')
245 source = bundle[:last_colon]
246 autostart = bundle[last_colon+1:]
247 else:
248 source = bundle
249 autostart = ""
250 remote_urls.append(source)
251 else:
252 # Local: "myapp" or "myapp:autostart"
253 parts = bundle.split(':')
254 source = parts[0]
255 autostart = parts[1] if len(parts) > 1 else ""
256 local_recipes.append(source)
257
258 # Store normalized format: source:runtime:autostart (for metadata file)
259 processed_bundles.append(f"{source}:{runtime}:{autostart}" if autostart else f"{source}:{runtime}")
260
261 # Add dependencies for local container recipes
262 # Local containers are built in the MAIN context (not multiconfig)
263 # and their OCI images are in main DEPLOY_DIR_IMAGE
264 if local_recipes:
265 deps = ""
266 for recipe in local_recipes:
267 # Container recipes produce OCI images via do_image_complete
268 deps += f" {recipe}:do_image_complete"
269 if deps:
270 d.appendVarFlag('do_compile', 'depends', deps)
271
272 # Store parsed lists for tasks
273 d.setVar('_LOCAL_CONTAINERS', ' '.join(local_recipes))
274 d.setVar('_REMOTE_CONTAINERS', ' '.join(remote_urls))
275 d.setVar('_PROCESSED_BUNDLES', ' '.join(processed_bundles))
276 d.setVar('_BUNDLE_RUNTIME', runtime)
277}
278
279# S must be a real directory
280S = "${WORKDIR}/sources"
281B = "${WORKDIR}/build"
282
283do_unpack[noexec] = "1"
284do_patch[noexec] = "1"
285do_configure[noexec] = "1"
286
287python do_fetch_containers() {
288 import subprocess
289 import os
290
291 remote_containers = (d.getVar('_REMOTE_CONTAINERS') or "").split()
292 if not remote_containers:
293 return
294
295 workdir = d.getVar('WORKDIR')
296 fetched_dir = os.path.join(workdir, 'fetched')
297 os.makedirs(fetched_dir, exist_ok=True)
298
299 # Find skopeo in native sysroot (available after do_prepare_recipe_sysroot)
300 # skopeo-native installs to sbindir, not bindir
301 staging_sbindir = d.getVar('STAGING_SBINDIR_NATIVE')
302 skopeo = os.path.join(staging_sbindir, 'skopeo')
303
304 for url in remote_containers:
305 if not url:
306 continue
307
308 # Digest is REQUIRED for remote containers
309 # Varflag key must be sanitized (no / or : allowed in BitBake varflag names)
310 sanitized_key = url.replace('/', '_').replace(':', '_')
311 digest = d.getVarFlag('CONTAINER_DIGESTS', sanitized_key)
312 if not digest:
313 bb.fatal(f"Remote container '{url}' requires a pinned digest.\n"
314 f"Add: CONTAINER_DIGESTS[{sanitized_key}] = \"sha256:...\"\n"
315 f"Get digest with: skopeo inspect docker://{url} | jq -r '.Digest'")
316
317 # Emit licensing warning
318 bb.warn(f"Fetching third-party container: {url}\n"
319 f"Ensure you have rights to redistribute this container in your image.\n"
320 f"Check the container's license terms before distribution.")
321
322 # Strip tag from URL when using digest (skopeo doesn't support both)
323 # e.g., docker.io/library/busybox:1.36 -> docker.io/library/busybox
324 base_url = url.rsplit(':', 1)[0] if ':' in url.split('/')[-1] else url
325 src = f"{base_url}@{digest}"
326 name = url.replace('/', '_').replace(':', '_')
327 dest_dir = os.path.join(fetched_dir, name)
328 dest = f"oci:{dest_dir}:latest"
329
330 bb.note(f"Fetching {src} -> {dest}")
331
332 try:
333 subprocess.check_call([skopeo, 'copy', f'docker://{src}', dest])
334 except subprocess.CalledProcessError as e:
335 bb.fatal(f"Failed to fetch container '{url}': {e}")
336}
337
338do_fetch_containers[network] = "1"
339addtask fetch_containers after do_prepare_recipe_sysroot before do_compile
340
341do_compile() {
342 set -e
343
344 mkdir -p "${S}"
345
346 # Clean OCI directory to avoid nested copies from incremental builds
347 rm -rf "${B}/oci"
348 mkdir -p "${B}/oci"
349
350 # Clear refs file to avoid duplicates from incremental builds
351 : > "${B}/oci-refs.txt"
352
353 RUNTIME="${_BUNDLE_RUNTIME}"
354 bbnote "Collecting OCI images for runtime: ${RUNTIME}"
355
356 # Collect OCI directories - NO vrunner here, just copy OCI images
357 # vrunner will be run ONCE by container-cross-install at rootfs time
358 for bundle in ${_PROCESSED_BUNDLES}; do
359 # Extract source from bundle format
360 source=$(echo "$bundle" | sed -E 's/:(docker|podman)(:(autostart|always|unless-stopped|on-failure|no))?$//')
361 collect_oci "$source"
362 done
363
364 # Store metadata for autostart processing (one bundle per line)
365 printf '%s\n' ${_PROCESSED_BUNDLES} > "${B}/bundle-metadata.txt"
366}
367
368collect_oci() {
369 local source="$1"
370
371 # Determine OCI directory and image reference
372 if echo "$source" | grep -qE '[/.]'; then
373 # Remote container - already fetched to WORKDIR/fetched/
374 local name=$(echo "$source" | sed 's|[/:]|_|g')
375 local oci_src="${WORKDIR}/fetched/${name}"
376 local tag=$(echo "$source" | grep -oE ':[^:]+$' | sed 's/^://' || echo "latest")
377 local base_name=$(echo "$source" | sed 's|.*/||' | sed 's/:.*$//')
378 local image_ref="${base_name}:${tag}"
379 else
380 # Local container - from DEPLOY_DIR
381 local oci_src="${DEPLOY_DIR_IMAGE}/${source}-latest-oci"
382 if [ ! -d "${oci_src}" ]; then
383 oci_src="${DEPLOY_DIR_IMAGE}/${source}-oci"
384 fi
385 if [ ! -d "${oci_src}" ]; then
386 oci_src="${DEPLOY_DIR_IMAGE}/${source}"
387 fi
388 local image_ref="${source}:latest"
389 fi
390
391 if [ ! -d "${oci_src}" ]; then
392 bbfatal "Container OCI directory not found: ${oci_src}"
393 fi
394
395 # Copy OCI directory to build dir with image ref as name
396 # Format: image_ref (e.g., busybox:1.36 or container-base:latest)
397 local oci_name=$(echo "${image_ref}" | sed 's|[/:]|_|g')
398 local oci_dest="${B}/oci/${oci_name}"
399
400 bbnote "Collecting OCI: ${oci_src} -> ${oci_dest} (ref: ${image_ref})"
401 cp -rL "${oci_src}" "${oci_dest}"
402
403 # Store the image reference for later use
404 echo "${oci_name}:${image_ref}" >> "${B}/oci-refs.txt"
405}
406
407do_install() {
408 # Install OCI directories for container-cross-install to process
409 # NO storage tars - vrunner runs once at rootfs time
410
411 RUNTIME="${_BUNDLE_RUNTIME}"
412
413 # Install OCI directories
414 if [ -d "${B}/oci" ] && [ -n "$(ls -A ${B}/oci 2>/dev/null)" ]; then
415 install -d ${D}${datadir}/container-bundles/${RUNTIME}/oci
416 cp -r ${B}/oci/* ${D}${datadir}/container-bundles/${RUNTIME}/oci/
417 fi
418
419 # Install OCI references file
420 if [ -f "${B}/oci-refs.txt" ]; then
421 install -d ${D}${datadir}/container-bundles/${RUNTIME}
422 install -m 0644 ${B}/oci-refs.txt \
423 ${D}${datadir}/container-bundles/${RUNTIME}/${PN}.refs
424 fi
425
426 # Install metadata for autostart service generation
427 if [ -f "${B}/bundle-metadata.txt" ]; then
428 install -d ${D}${datadir}/container-bundles
429 install -m 0644 ${B}/bundle-metadata.txt \
430 ${D}${datadir}/container-bundles/${PN}.meta
431 fi
432}
433
434FILES:${PN} = "${datadir}/container-bundles"
435
436# Automatically trigger multiconfig blob builds
437# Note: This does NOT create circular dependencies because the blob build chain
438# (vdkr/vpdmn-initramfs-create -> vdkr/vpdmn-rootfs-image) is completely separate
439# from container image recipes. Circular deps only occur if bundle packages are
440# globally added to all images (including container images themselves).
441do_compile[mcdepends] = "mc::${VRUNTIME_MULTICONFIG}:vdkr-initramfs-create:do_deploy mc::${VRUNTIME_MULTICONFIG}:vpdmn-initramfs-create:do_deploy"
diff --git a/recipes-demo/autostart-test/autostart-test_1.0.bb b/recipes-demo/autostart-test/autostart-test_1.0.bb
new file mode 100644
index 00000000..38086023
--- /dev/null
+++ b/recipes-demo/autostart-test/autostart-test_1.0.bb
@@ -0,0 +1,16 @@
1SUMMARY = "Simple test service for container autostart verification"
2DESCRIPTION = "A shell script that runs continuously and logs timestamps, \
3useful for verifying container autostart functionality."
4LICENSE = "MIT"
5LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/MIT;md5=0835ade698e0bcf8506ecda2f7b4f302"
6
7SRC_URI = "file://autostart-test.sh"
8
9S = "${UNPACKDIR}"
10
11do_install() {
12 install -d ${D}${bindir}
13 install -m 0755 ${S}/autostart-test.sh ${D}${bindir}/autostart-test
14}
15
16RDEPENDS:${PN} = "busybox"
diff --git a/recipes-demo/autostart-test/files/autostart-test.sh b/recipes-demo/autostart-test/files/autostart-test.sh
new file mode 100644
index 00000000..30043fa7
--- /dev/null
+++ b/recipes-demo/autostart-test/files/autostart-test.sh
@@ -0,0 +1,21 @@
1#!/bin/sh
2#
3# Simple test service for verifying container autostart
4# Writes timestamps to stdout (captured by container logs)
5#
6
7INTERVAL=${INTERVAL:-5}
8MESSAGE=${MESSAGE:-"autostart-test is running"}
9
10echo "=== Autostart Test Service ==="
11echo "Started at: $(date)"
12echo "PID: $$"
13echo "Interval: ${INTERVAL}s"
14echo "=============================="
15
16count=0
17while true; do
18 count=$((count + 1))
19 echo "[${count}] $(date): ${MESSAGE}"
20 sleep ${INTERVAL}
21done
diff --git a/recipes-demo/images/autostart-test-container.bb b/recipes-demo/images/autostart-test-container.bb
new file mode 100644
index 00000000..73a29a49
--- /dev/null
+++ b/recipes-demo/images/autostart-test-container.bb
@@ -0,0 +1,24 @@
1SUMMARY = "Autostart test container"
2DESCRIPTION = "A container for testing autostart functionality. \
3Runs a simple service that logs timestamps continuously."
4LICENSE = "MIT"
5LIC_FILES_CHKSUM = "file://${COREBASE}/meta/COPYING.MIT;md5=3da9cfbcb788c80a0384361b4de20420"
6
7# Inherit from container-app-base for standard container setup
8require recipes-extended/images/container-app-base.bb
9
10# The test service that runs continuously
11CONTAINER_APP = "autostart-test"
12CONTAINER_APP_CMD = "/usr/bin/autostart-test"
13
14# To test autostart, add to local.conf:
15# BUNDLED_CONTAINERS = "autostart-test-container-latest-oci:docker:autostart"
16#
17# Then verify on target:
18# docker ps # Should show container running
19# docker logs autostart-test-container # Should show timestamp logs
20#
21# For Podman:
22# BUNDLED_CONTAINERS = "autostart-test-container-latest-oci:podman:autostart"
23# podman ps
24# podman logs autostart-test-container
diff --git a/recipes-extended/container-bundles/example-container-bundle_1.0.bb b/recipes-extended/container-bundles/example-container-bundle_1.0.bb
new file mode 100644
index 00000000..a91e4609
--- /dev/null
+++ b/recipes-extended/container-bundles/example-container-bundle_1.0.bb
@@ -0,0 +1,68 @@
1# example-container-bundle_1.0.bb
2# ===========================================================================
3# Example container bundle recipe demonstrating container-bundle.bbclass
4# ===========================================================================
5#
6# This recipe shows how to create a package that bundles containers.
7# When installed via IMAGE_INSTALL, the containers are automatically
8# merged into the target image's container storage.
9#
10# Usage in image recipe (e.g., container-image-host.bb):
11# IMAGE_INSTALL += "example-container-bundle"
12#
13# Or in local.conf (use pn- override for specific images):
14# IMAGE_INSTALL:append:pn-container-image-host = " example-container-bundle"
15#
16# IMPORTANT: Do NOT use global IMAGE_INSTALL:append without pn- override!
17# This causes circular dependencies when container images try to include
18# the bundle that depends on them.
19#
20# ===========================================================================
21
22SUMMARY = "Example container bundle"
23DESCRIPTION = "Demonstrates container-bundle.bbclass by bundling the \
24 container-base image. Use this as a template for your \
25 own container bundles."
26HOMEPAGE = "https://github.com/anthropics/meta-virtualization"
27LICENSE = "MIT"
28LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/MIT;md5=0835ade698e0bcf8506ecda2f7b4f302"
29
30inherit container-bundle
31
32# Define containers to bundle
33# Format: source[:autostart-policy]
34#
35# source: Either a local recipe name or registry URL
36# - Local: "container-base" (simple recipe name)
37# - Remote: "docker.io/library/alpine:3.19" (registry URL)
38#
39# autostart: (optional) autostart | always | unless-stopped | on-failure
40#
41# Runtime is determined automatically from CONTAINER_PROFILE (or CONTAINER_BUNDLE_RUNTIME)
42
43# Bundle the test containers we've been using:
44# - container-base: minimal busybox container
45# - container-app-base: busybox with app structure
46# - autostart-test-container: container that logs startup for autostart testing
47CONTAINER_BUNDLES = "\
48 container-base \
49 container-app-base \
50 autostart-test-container:autostart \
51"
52
53# Override runtime if needed (uncomment to force a specific runtime):
54# CONTAINER_BUNDLE_RUNTIME = "podman"
55
56# For remote containers (not used in this example), you MUST provide digests:
57# CONTAINER_DIGESTS[docker.io/library/redis:7] = "sha256:e422889e156e..."
58#
59# Get the digest with:
60# skopeo inspect docker://docker.io/library/redis:7 | jq -r '.Digest'
61
62# Example with multiple containers and autostart:
63# CONTAINER_BUNDLES = "\
64# myapp:autostart \
65# mydb \
66# docker.io/library/redis:7 \
67# "
68# CONTAINER_DIGESTS[docker.io/library/redis:7] = "sha256:e422889..."
diff --git a/recipes-extended/container-bundles/remote-container-bundle_1.0.bb b/recipes-extended/container-bundles/remote-container-bundle_1.0.bb
new file mode 100644
index 00000000..7da267e5
--- /dev/null
+++ b/recipes-extended/container-bundles/remote-container-bundle_1.0.bb
@@ -0,0 +1,50 @@
1# SPDX-FileCopyrightText: Copyright (C) 2025 Bruce Ashfield
2#
3# SPDX-License-Identifier: MIT
4#
5# remote-container-bundle_1.0.bb
6# ===========================================================================
7# Test recipe for remote container fetching via container-bundle.bbclass
8# ===========================================================================
9#
10# This recipe demonstrates and tests fetching containers from a remote
11# registry during the Yocto build. The container is pulled via skopeo
12# and bundled into a package that can be installed into target images.
13#
14# Usage in image recipe:
15# IMAGE_INSTALL += "remote-container-bundle"
16#
17# Or in local.conf:
18# IMAGE_INSTALL:append:pn-container-image-host = " remote-container-bundle"
19#
20# The container will be available as "busybox:1.36" in the target's
21# Docker/Podman storage after boot.
22#
23# ===========================================================================
24
25SUMMARY = "Remote container bundle test"
26DESCRIPTION = "Tests container-bundle.bbclass remote container fetching. \
27 Pulls busybox from docker.io and bundles it for installation."
28HOMEPAGE = "https://github.com/anthropics/meta-virtualization"
29LICENSE = "MIT"
30LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/MIT;md5=0835ade698e0bcf8506ecda2f7b4f302"
31
32inherit container-bundle
33
34# Remote container from Docker Hub
35# Using busybox as it's small (~2MB) and available for multiple architectures
36CONTAINER_BUNDLES = "\
37 docker.io/library/busybox:1.36 \
38"
39
40# REQUIRED: Pinned digest for reproducible builds
41# Get with: skopeo inspect docker://docker.io/library/busybox:1.36 | jq -r '.Digest'
42# Note: This is the multi-arch manifest digest, skopeo will select the correct arch
43# Key format: Replace / and : with _ for BitBake variable flag compatibility
44CONTAINER_DIGESTS[docker.io_library_busybox_1.36] = "sha256:768e5c6f5cb6db0794eec98dc7a967f40631746c32232b78a3105fb946f3ab83"
45
46# Note: busybox is GPL-licensed, so no LICENSE_FLAGS needed.
47# For containers with commercial licenses, you would add:
48# LICENSE_FLAGS:append = " commercial"
49# And accept in local.conf:
50# LICENSE_FLAGS_ACCEPTED:append = " commercial"