diff options
| author | Bruce Ashfield <bruce.ashfield@gmail.com> | 2026-01-12 16:09:12 +0000 |
|---|---|---|
| committer | Bruce Ashfield <bruce.ashfield@gmail.com> | 2026-02-09 03:32:52 +0000 |
| commit | 87ed625c043e4cdbabf569227b189823cd08db8e (patch) | |
| tree | a307f96f218f3be0e2741fe13079400b24ee8487 /classes | |
| parent | 33944038c68d8e497e8dd9861c5ca6c4da7d48e5 (diff) | |
| download | meta-virtualization-87ed625c043e4cdbabf569227b189823cd08db8e.tar.gz | |
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 <bruce.ashfield@gmail.com>
Diffstat (limited to 'classes')
| -rw-r--r-- | classes/container-registry.bbclass | 203 |
1 files changed, 203 insertions, 0 deletions
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 @@ | |||
| 1 | # SPDX-FileCopyrightText: Copyright (C) 2025 Bruce Ashfield | ||
| 2 | # | ||
| 3 | # SPDX-License-Identifier: MIT | ||
| 4 | # | ||
| 5 | # container-registry.bbclass | ||
| 6 | # =========================================================================== | ||
| 7 | # Container registry operations for pushing OCI images to registries | ||
| 8 | # =========================================================================== | ||
| 9 | # | ||
| 10 | # This class provides functions to push OCI images from the deploy directory | ||
| 11 | # to a container registry. It works with docker-distribution, Docker Hub, | ||
| 12 | # or any OCI-compliant registry. | ||
| 13 | # | ||
| 14 | # Usage: | ||
| 15 | # inherit container-registry | ||
| 16 | # | ||
| 17 | # # In do_populate_registry task: | ||
| 18 | # container_registry_push(d, oci_path, image_name) | ||
| 19 | # | ||
| 20 | # Configuration: | ||
| 21 | # CONTAINER_REGISTRY_URL = "localhost:5000" # Registry endpoint | ||
| 22 | # CONTAINER_REGISTRY_NAMESPACE = "yocto" # Image namespace | ||
| 23 | # CONTAINER_REGISTRY_TLS_VERIFY = "false" # TLS verification | ||
| 24 | # CONTAINER_REGISTRY_TAG_STRATEGY = "timestamp latest" # Tag generation | ||
| 25 | # CONTAINER_REGISTRY_STORAGE = "${TOPDIR}/container-registry" # Persistent storage | ||
| 26 | # | ||
| 27 | # =========================================================================== | ||
| 28 | |||
| 29 | # Registry configuration | ||
| 30 | CONTAINER_REGISTRY_URL ?= "localhost:5000" | ||
| 31 | CONTAINER_REGISTRY_NAMESPACE ?= "yocto" | ||
| 32 | CONTAINER_REGISTRY_TLS_VERIFY ?= "false" | ||
| 33 | CONTAINER_REGISTRY_TAG_STRATEGY ?= "timestamp latest" | ||
| 34 | |||
| 35 | # Storage location for registry data (default: outside tmp/, persists across builds) | ||
| 36 | # Set in local.conf to customize, e.g.: | ||
| 37 | # CONTAINER_REGISTRY_STORAGE = "/data/container-registry" | ||
| 38 | # CONTAINER_REGISTRY_STORAGE = "${TOPDIR}/../container-registry" | ||
| 39 | CONTAINER_REGISTRY_STORAGE ?= "${TOPDIR}/container-registry" | ||
| 40 | |||
| 41 | # Require skopeo-native for registry operations | ||
| 42 | DEPENDS += "skopeo-native" | ||
| 43 | |||
| 44 | def container_registry_generate_tags(d, image_name): | ||
| 45 | """Generate tags based on CONTAINER_REGISTRY_TAG_STRATEGY. | ||
| 46 | |||
| 47 | Strategies: | ||
| 48 | timestamp - YYYYMMDD-HHMMSS format | ||
| 49 | git - Short git hash if in git repo | ||
| 50 | version - PV from recipe or image name | ||
| 51 | latest - Always includes 'latest' tag | ||
| 52 | arch - Appends architecture suffix | ||
| 53 | |||
| 54 | Returns list of tags to apply. | ||
| 55 | """ | ||
| 56 | import datetime | ||
| 57 | import subprocess | ||
| 58 | |||
| 59 | strategy = (d.getVar('CONTAINER_REGISTRY_TAG_STRATEGY') or 'latest').split() | ||
| 60 | tags = [] | ||
| 61 | |||
| 62 | for strat in strategy: | ||
| 63 | if strat == 'timestamp': | ||
| 64 | ts = datetime.datetime.now().strftime('%Y%m%d-%H%M%S') | ||
| 65 | tags.append(ts) | ||
| 66 | elif strat == 'git': | ||
| 67 | try: | ||
| 68 | git_hash = subprocess.check_output( | ||
| 69 | ['git', 'rev-parse', '--short', 'HEAD'], | ||
| 70 | stderr=subprocess.DEVNULL, | ||
| 71 | cwd=d.getVar('TOPDIR') | ||
| 72 | ).decode().strip() | ||
| 73 | if git_hash: | ||
| 74 | tags.append(git_hash) | ||
| 75 | except (subprocess.CalledProcessError, FileNotFoundError): | ||
| 76 | pass | ||
| 77 | elif strat == 'version': | ||
| 78 | pv = d.getVar('PV') | ||
| 79 | if pv and pv != '1.0': | ||
| 80 | tags.append(pv) | ||
| 81 | elif strat == 'latest': | ||
| 82 | tags.append('latest') | ||
| 83 | elif strat == 'arch': | ||
| 84 | arch = d.getVar('TARGET_ARCH') or d.getVar('BUILD_ARCH') | ||
| 85 | if arch: | ||
| 86 | # Add arch suffix to existing tags | ||
| 87 | arch_tags = [f"{t}-{arch}" for t in tags if t != 'latest'] | ||
| 88 | tags.extend(arch_tags) | ||
| 89 | |||
| 90 | # Ensure at least one tag | ||
| 91 | if not tags: | ||
| 92 | tags = ['latest'] | ||
| 93 | |||
| 94 | return tags | ||
| 95 | |||
| 96 | def container_registry_push(d, oci_path, image_name, tags=None): | ||
| 97 | """Push an OCI image to the configured registry. | ||
| 98 | |||
| 99 | Args: | ||
| 100 | d: BitBake datastore | ||
| 101 | oci_path: Path to OCI directory (containing index.json) | ||
| 102 | image_name: Name for the image (without registry/namespace) | ||
| 103 | tags: Optional list of tags (default: generated from strategy) | ||
| 104 | |||
| 105 | Returns: | ||
| 106 | List of pushed image references (registry/namespace/name:tag) | ||
| 107 | """ | ||
| 108 | import os | ||
| 109 | import subprocess | ||
| 110 | |||
| 111 | registry = d.getVar('CONTAINER_REGISTRY_URL') | ||
| 112 | namespace = d.getVar('CONTAINER_REGISTRY_NAMESPACE') | ||
| 113 | tls_verify = d.getVar('CONTAINER_REGISTRY_TLS_VERIFY') | ||
| 114 | |||
| 115 | # Find skopeo in native sysroot | ||
| 116 | staging_sbindir = d.getVar('STAGING_SBINDIR_NATIVE') | ||
| 117 | skopeo = os.path.join(staging_sbindir, 'skopeo') | ||
| 118 | |||
| 119 | if not os.path.exists(skopeo): | ||
| 120 | bb.fatal(f"skopeo not found at {skopeo} - ensure skopeo-native is built") | ||
| 121 | |||
| 122 | # Validate OCI directory | ||
| 123 | index_json = os.path.join(oci_path, 'index.json') | ||
| 124 | if not os.path.exists(index_json): | ||
| 125 | bb.fatal(f"Invalid OCI directory: {oci_path} (missing index.json)") | ||
| 126 | |||
| 127 | # Generate tags if not provided | ||
| 128 | if tags is None: | ||
| 129 | tags = container_registry_generate_tags(d, image_name) | ||
| 130 | |||
| 131 | pushed = [] | ||
| 132 | src = f"oci:{oci_path}" | ||
| 133 | |||
| 134 | for tag in tags: | ||
| 135 | dest = f"docker://{registry}/{namespace}/{image_name}:{tag}" | ||
| 136 | |||
| 137 | cmd = [skopeo, 'copy'] | ||
| 138 | if tls_verify == 'false': | ||
| 139 | cmd.append('--dest-tls-verify=false') | ||
| 140 | cmd.extend([src, dest]) | ||
| 141 | |||
| 142 | bb.note(f"Pushing {image_name}:{tag} to {registry}/{namespace}/") | ||
| 143 | |||
| 144 | try: | ||
| 145 | subprocess.check_call(cmd) | ||
| 146 | pushed.append(f"{registry}/{namespace}/{image_name}:{tag}") | ||
| 147 | bb.note(f"Successfully pushed {dest}") | ||
| 148 | except subprocess.CalledProcessError as e: | ||
| 149 | bb.error(f"Failed to push {dest}: {e}") | ||
| 150 | |||
| 151 | return pushed | ||
| 152 | |||
| 153 | def container_registry_discover_oci_images(d): | ||
| 154 | """Discover OCI images in the deploy directory. | ||
| 155 | |||
| 156 | Finds directories matching *-oci or *-latest-oci patterns | ||
| 157 | that contain valid OCI layouts (index.json). | ||
| 158 | |||
| 159 | Returns: | ||
| 160 | List of tuples: (oci_path, image_name) | ||
| 161 | """ | ||
| 162 | import os | ||
| 163 | |||
| 164 | deploy_dir = d.getVar('DEPLOY_DIR_IMAGE') | ||
| 165 | if not deploy_dir or not os.path.isdir(deploy_dir): | ||
| 166 | return [] | ||
| 167 | |||
| 168 | images = [] | ||
| 169 | |||
| 170 | for entry in os.listdir(deploy_dir): | ||
| 171 | # Match *-oci or *-latest-oci directories | ||
| 172 | if not (entry.endswith('-oci') or entry.endswith('-latest-oci')): | ||
| 173 | continue | ||
| 174 | |||
| 175 | oci_path = os.path.join(deploy_dir, entry) | ||
| 176 | if not os.path.isdir(oci_path): | ||
| 177 | continue | ||
| 178 | |||
| 179 | # Verify valid OCI layout | ||
| 180 | if not os.path.exists(os.path.join(oci_path, 'index.json')): | ||
| 181 | continue | ||
| 182 | |||
| 183 | # Extract image name from directory name | ||
| 184 | # container-base-qemux86-64.rootfs-20260108.rootfs-oci -> container-base | ||
| 185 | # container-base-latest-oci -> container-base | ||
| 186 | name = entry | ||
| 187 | for suffix in ['-latest-oci', '-oci']: | ||
| 188 | if name.endswith(suffix): | ||
| 189 | name = name[:-len(suffix)] | ||
| 190 | break | ||
| 191 | |||
| 192 | # Remove machine suffix if present (e.g., -qemux86-64) | ||
| 193 | machine = d.getVar('MACHINE') | ||
| 194 | if machine and f'-{machine}' in name: | ||
| 195 | name = name.split(f'-{machine}')[0] | ||
| 196 | |||
| 197 | # Remove rootfs timestamp suffix (e.g., .rootfs-20260108) | ||
| 198 | if '.rootfs-' in name: | ||
| 199 | name = name.split('.rootfs-')[0] | ||
| 200 | |||
| 201 | images.append((oci_path, name)) | ||
| 202 | |||
| 203 | return images | ||
