From 87ed625c043e4cdbabf569227b189823cd08db8e Mon Sep 17 00:00:00 2001 From: Bruce Ashfield Date: Mon, 12 Jan 2026 16:09:12 +0000 Subject: container-registry: add local OCI registry infrastructure Add container registry support for Yocto container workflows: - container-registry.bbclass with helper functions - container-registry-index.bb generates helper script with baked paths - docker-registry-config.bb for Docker daemon on targets - container-oci-registry-config.bb for Podman/Skopeo/Buildah targets - IMAGE_FEATURES container-registry for easy target configuration Signed-off-by: Bruce Ashfield --- classes/container-registry.bbclass | 203 +++++++++++++++++++++++++++++++++++++ 1 file changed, 203 insertions(+) create mode 100644 classes/container-registry.bbclass (limited to 'classes') diff --git a/classes/container-registry.bbclass b/classes/container-registry.bbclass new file mode 100644 index 00000000..f5a2d0c3 --- /dev/null +++ b/classes/container-registry.bbclass @@ -0,0 +1,203 @@ +# SPDX-FileCopyrightText: Copyright (C) 2025 Bruce Ashfield +# +# SPDX-License-Identifier: MIT +# +# container-registry.bbclass +# =========================================================================== +# Container registry operations for pushing OCI images to registries +# =========================================================================== +# +# This class provides functions to push OCI images from the deploy directory +# to a container registry. It works with docker-distribution, Docker Hub, +# or any OCI-compliant registry. +# +# Usage: +# inherit container-registry +# +# # In do_populate_registry task: +# container_registry_push(d, oci_path, image_name) +# +# Configuration: +# CONTAINER_REGISTRY_URL = "localhost:5000" # Registry endpoint +# CONTAINER_REGISTRY_NAMESPACE = "yocto" # Image namespace +# CONTAINER_REGISTRY_TLS_VERIFY = "false" # TLS verification +# CONTAINER_REGISTRY_TAG_STRATEGY = "timestamp latest" # Tag generation +# CONTAINER_REGISTRY_STORAGE = "${TOPDIR}/container-registry" # Persistent storage +# +# =========================================================================== + +# Registry configuration +CONTAINER_REGISTRY_URL ?= "localhost:5000" +CONTAINER_REGISTRY_NAMESPACE ?= "yocto" +CONTAINER_REGISTRY_TLS_VERIFY ?= "false" +CONTAINER_REGISTRY_TAG_STRATEGY ?= "timestamp latest" + +# Storage location for registry data (default: outside tmp/, persists across builds) +# Set in local.conf to customize, e.g.: +# CONTAINER_REGISTRY_STORAGE = "/data/container-registry" +# CONTAINER_REGISTRY_STORAGE = "${TOPDIR}/../container-registry" +CONTAINER_REGISTRY_STORAGE ?= "${TOPDIR}/container-registry" + +# Require skopeo-native for registry operations +DEPENDS += "skopeo-native" + +def container_registry_generate_tags(d, image_name): + """Generate tags based on CONTAINER_REGISTRY_TAG_STRATEGY. + + Strategies: + timestamp - YYYYMMDD-HHMMSS format + git - Short git hash if in git repo + version - PV from recipe or image name + latest - Always includes 'latest' tag + arch - Appends architecture suffix + + Returns list of tags to apply. + """ + import datetime + import subprocess + + strategy = (d.getVar('CONTAINER_REGISTRY_TAG_STRATEGY') or 'latest').split() + tags = [] + + for strat in strategy: + if strat == 'timestamp': + ts = datetime.datetime.now().strftime('%Y%m%d-%H%M%S') + tags.append(ts) + elif strat == 'git': + try: + git_hash = subprocess.check_output( + ['git', 'rev-parse', '--short', 'HEAD'], + stderr=subprocess.DEVNULL, + cwd=d.getVar('TOPDIR') + ).decode().strip() + if git_hash: + tags.append(git_hash) + except (subprocess.CalledProcessError, FileNotFoundError): + pass + elif strat == 'version': + pv = d.getVar('PV') + if pv and pv != '1.0': + tags.append(pv) + elif strat == 'latest': + tags.append('latest') + elif strat == 'arch': + arch = d.getVar('TARGET_ARCH') or d.getVar('BUILD_ARCH') + if arch: + # Add arch suffix to existing tags + arch_tags = [f"{t}-{arch}" for t in tags if t != 'latest'] + tags.extend(arch_tags) + + # Ensure at least one tag + if not tags: + tags = ['latest'] + + return tags + +def container_registry_push(d, oci_path, image_name, tags=None): + """Push an OCI image to the configured registry. + + Args: + d: BitBake datastore + oci_path: Path to OCI directory (containing index.json) + image_name: Name for the image (without registry/namespace) + tags: Optional list of tags (default: generated from strategy) + + Returns: + List of pushed image references (registry/namespace/name:tag) + """ + import os + import subprocess + + registry = d.getVar('CONTAINER_REGISTRY_URL') + namespace = d.getVar('CONTAINER_REGISTRY_NAMESPACE') + tls_verify = d.getVar('CONTAINER_REGISTRY_TLS_VERIFY') + + # Find skopeo in native sysroot + staging_sbindir = d.getVar('STAGING_SBINDIR_NATIVE') + skopeo = os.path.join(staging_sbindir, 'skopeo') + + if not os.path.exists(skopeo): + bb.fatal(f"skopeo not found at {skopeo} - ensure skopeo-native is built") + + # Validate OCI directory + index_json = os.path.join(oci_path, 'index.json') + if not os.path.exists(index_json): + bb.fatal(f"Invalid OCI directory: {oci_path} (missing index.json)") + + # Generate tags if not provided + if tags is None: + tags = container_registry_generate_tags(d, image_name) + + pushed = [] + src = f"oci:{oci_path}" + + for tag in tags: + dest = f"docker://{registry}/{namespace}/{image_name}:{tag}" + + cmd = [skopeo, 'copy'] + if tls_verify == 'false': + cmd.append('--dest-tls-verify=false') + cmd.extend([src, dest]) + + bb.note(f"Pushing {image_name}:{tag} to {registry}/{namespace}/") + + try: + subprocess.check_call(cmd) + pushed.append(f"{registry}/{namespace}/{image_name}:{tag}") + bb.note(f"Successfully pushed {dest}") + except subprocess.CalledProcessError as e: + bb.error(f"Failed to push {dest}: {e}") + + return pushed + +def container_registry_discover_oci_images(d): + """Discover OCI images in the deploy directory. + + Finds directories matching *-oci or *-latest-oci patterns + that contain valid OCI layouts (index.json). + + Returns: + List of tuples: (oci_path, image_name) + """ + import os + + deploy_dir = d.getVar('DEPLOY_DIR_IMAGE') + if not deploy_dir or not os.path.isdir(deploy_dir): + return [] + + images = [] + + for entry in os.listdir(deploy_dir): + # Match *-oci or *-latest-oci directories + if not (entry.endswith('-oci') or entry.endswith('-latest-oci')): + continue + + oci_path = os.path.join(deploy_dir, entry) + if not os.path.isdir(oci_path): + continue + + # Verify valid OCI layout + if not os.path.exists(os.path.join(oci_path, 'index.json')): + continue + + # Extract image name from directory name + # container-base-qemux86-64.rootfs-20260108.rootfs-oci -> container-base + # container-base-latest-oci -> container-base + name = entry + for suffix in ['-latest-oci', '-oci']: + if name.endswith(suffix): + name = name[:-len(suffix)] + break + + # Remove machine suffix if present (e.g., -qemux86-64) + machine = d.getVar('MACHINE') + if machine and f'-{machine}' in name: + name = name.split(f'-{machine}')[0] + + # Remove rootfs timestamp suffix (e.g., .rootfs-20260108) + if '.rootfs-' in name: + name = name.split('.rootfs-')[0] + + images.append((oci_path, name)) + + return images -- cgit v1.2.3-54-g00ecf