diff options
Diffstat (limited to 'recipes-containers/container-registry/container-registry-index.bb')
| -rw-r--r-- | recipes-containers/container-registry/container-registry-index.bb | 433 |
1 files changed, 433 insertions, 0 deletions
diff --git a/recipes-containers/container-registry/container-registry-index.bb b/recipes-containers/container-registry/container-registry-index.bb new file mode 100644 index 00000000..c3a48e94 --- /dev/null +++ b/recipes-containers/container-registry/container-registry-index.bb | |||
| @@ -0,0 +1,433 @@ | |||
| 1 | # SPDX-FileCopyrightText: Copyright (C) 2025 Bruce Ashfield | ||
| 2 | # | ||
| 3 | # SPDX-License-Identifier: MIT | ||
| 4 | # | ||
| 5 | # container-registry-index.bb | ||
| 6 | # =========================================================================== | ||
| 7 | # Push OCI container images to a registry (like package-index for containers) | ||
| 8 | # =========================================================================== | ||
| 9 | # | ||
| 10 | # This is the container equivalent of meta/recipes-core/meta/package-index.bb | ||
| 11 | # It discovers OCI images in DEPLOY_DIR_IMAGE and pushes them to a registry. | ||
| 12 | # | ||
| 13 | # Usage: | ||
| 14 | # # Start registry first (separate terminal): | ||
| 15 | # oe-run-native docker-distribution-native registry serve config.yml | ||
| 16 | # | ||
| 17 | # # Push all container images to registry: | ||
| 18 | # bitbake container-registry-index | ||
| 19 | # | ||
| 20 | # # Or use the helper script: | ||
| 21 | # oe-run-native container-registry-index | ||
| 22 | # | ||
| 23 | # Configuration (in local.conf): | ||
| 24 | # CONTAINER_REGISTRY_URL = "localhost:5000" | ||
| 25 | # CONTAINER_REGISTRY_NAMESPACE = "yocto" | ||
| 26 | # CONTAINER_REGISTRY_IMAGES = "container-base container-app" # optional filter | ||
| 27 | # | ||
| 28 | # =========================================================================== | ||
| 29 | |||
| 30 | SUMMARY = "Populate container registry with OCI images" | ||
| 31 | LICENSE = "MIT" | ||
| 32 | |||
| 33 | INHIBIT_DEFAULT_DEPS = "1" | ||
| 34 | PACKAGES = "" | ||
| 35 | |||
| 36 | inherit nopackages container-registry | ||
| 37 | |||
| 38 | deltask do_fetch | ||
| 39 | deltask do_unpack | ||
| 40 | deltask do_patch | ||
| 41 | deltask do_configure | ||
| 42 | deltask do_compile | ||
| 43 | deltask do_install | ||
| 44 | deltask do_populate_lic | ||
| 45 | deltask do_populate_sysroot | ||
| 46 | |||
| 47 | do_container_registry_index[nostamp] = "1" | ||
| 48 | do_container_registry_index[network] = "1" | ||
| 49 | do_container_registry_index[depends] += "skopeo-native:do_populate_sysroot" | ||
| 50 | |||
| 51 | python do_container_registry_index() { | ||
| 52 | import os | ||
| 53 | |||
| 54 | registry = d.getVar('CONTAINER_REGISTRY_URL') | ||
| 55 | namespace = d.getVar('CONTAINER_REGISTRY_NAMESPACE') | ||
| 56 | specific_images = (d.getVar('CONTAINER_REGISTRY_IMAGES') or '').split() | ||
| 57 | |||
| 58 | bb.plain(f"Container Registry Index: {registry}/{namespace}/") | ||
| 59 | |||
| 60 | # Discover OCI images | ||
| 61 | all_images = container_registry_discover_oci_images(d) | ||
| 62 | |||
| 63 | if not all_images: | ||
| 64 | bb.warn("No OCI images found in deploy directory") | ||
| 65 | bb.plain(f"Deploy directory: {d.getVar('DEPLOY_DIR_IMAGE')}") | ||
| 66 | bb.plain("Build container images first: bitbake container-base") | ||
| 67 | return | ||
| 68 | |||
| 69 | bb.plain(f"Found {len(all_images)} OCI images") | ||
| 70 | |||
| 71 | # Filter if specific images requested | ||
| 72 | if specific_images: | ||
| 73 | images = [(path, name) for path, name in all_images if name in specific_images] | ||
| 74 | else: | ||
| 75 | images = all_images | ||
| 76 | |||
| 77 | # Push each image | ||
| 78 | pushed_refs = [] | ||
| 79 | for oci_path, image_name in images: | ||
| 80 | bb.plain(f"Pushing: {image_name}") | ||
| 81 | refs = container_registry_push(d, oci_path, image_name) | ||
| 82 | pushed_refs.extend(refs) | ||
| 83 | |||
| 84 | bb.plain(f"Pushed {len(pushed_refs)} image references to {registry}") | ||
| 85 | } | ||
| 86 | |||
| 87 | addtask do_container_registry_index before do_build | ||
| 88 | |||
| 89 | # Generate a helper script with paths baked in | ||
| 90 | # Script is placed alongside registry storage (outside tmp/) so it persists | ||
| 91 | CONTAINER_REGISTRY_SCRIPT = "${CONTAINER_REGISTRY_STORAGE}/container-registry.sh" | ||
| 92 | |||
| 93 | python do_generate_registry_script() { | ||
| 94 | import os | ||
| 95 | import stat | ||
| 96 | |||
| 97 | script_path = d.getVar('CONTAINER_REGISTRY_SCRIPT') | ||
| 98 | deploy_dir = d.getVar('DEPLOY_DIR') | ||
| 99 | deploy_dir_image = d.getVar('DEPLOY_DIR_IMAGE') | ||
| 100 | |||
| 101 | # Find registry binary path | ||
| 102 | native_sysroot = d.getVar('STAGING_DIR_NATIVE') or '' | ||
| 103 | registry_bin = os.path.join(native_sysroot, 'usr', 'sbin', 'registry') | ||
| 104 | |||
| 105 | # Find skopeo binary path | ||
| 106 | skopeo_bin = os.path.join(d.getVar('STAGING_SBINDIR_NATIVE') or '', 'skopeo') | ||
| 107 | |||
| 108 | # Config file path | ||
| 109 | config_file = os.path.join(d.getVar('THISDIR'), 'files', 'container-registry-dev.yml') | ||
| 110 | |||
| 111 | # Registry settings | ||
| 112 | registry_url = d.getVar('CONTAINER_REGISTRY_URL') | ||
| 113 | registry_namespace = d.getVar('CONTAINER_REGISTRY_NAMESPACE') | ||
| 114 | registry_storage = d.getVar('CONTAINER_REGISTRY_STORAGE') | ||
| 115 | |||
| 116 | os.makedirs(deploy_dir, exist_ok=True) | ||
| 117 | |||
| 118 | script = f'''#!/bin/bash | ||
| 119 | # Container Registry Helper Script | ||
| 120 | # Generated by: bitbake container-registry-index -c generate_registry_script | ||
| 121 | # | ||
| 122 | # This script has all paths pre-configured for your build. | ||
| 123 | # | ||
| 124 | # Usage: | ||
| 125 | # {script_path} start # Start registry server | ||
| 126 | # {script_path} stop # Stop registry server | ||
| 127 | # {script_path} status # Check if running | ||
| 128 | # {script_path} push # Push OCI images to registry | ||
| 129 | # {script_path} import <image> # Import 3rd party image | ||
| 130 | # {script_path} list # List all images with tags | ||
| 131 | # {script_path} tags <image> # List tags for an image | ||
| 132 | # {script_path} catalog # List image names (raw API) | ||
| 133 | |||
| 134 | set -e | ||
| 135 | |||
| 136 | # Pre-configured paths from bitbake | ||
| 137 | REGISTRY_BIN="{registry_bin}" | ||
| 138 | SKOPEO_BIN="{skopeo_bin}" | ||
| 139 | REGISTRY_CONFIG="{config_file}" | ||
| 140 | REGISTRY_STORAGE="{registry_storage}" | ||
| 141 | REGISTRY_URL="{registry_url}" | ||
| 142 | REGISTRY_NAMESPACE="{registry_namespace}" | ||
| 143 | DEPLOY_DIR_IMAGE="{deploy_dir_image}" | ||
| 144 | |||
| 145 | PID_FILE="/tmp/container-registry.pid" | ||
| 146 | LOG_FILE="/tmp/container-registry.log" | ||
| 147 | |||
| 148 | cmd_start() {{ | ||
| 149 | if [ -f "$PID_FILE" ] && kill -0 "$(cat $PID_FILE)" 2>/dev/null; then | ||
| 150 | echo "Registry already running (PID: $(cat $PID_FILE))" | ||
| 151 | return 0 | ||
| 152 | fi | ||
| 153 | |||
| 154 | if [ ! -x "$REGISTRY_BIN" ]; then | ||
| 155 | echo "Error: Registry binary not found at $REGISTRY_BIN" | ||
| 156 | echo "Build it with: bitbake docker-distribution-native" | ||
| 157 | return 1 | ||
| 158 | fi | ||
| 159 | |||
| 160 | mkdir -p "$REGISTRY_STORAGE" | ||
| 161 | export REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY="$REGISTRY_STORAGE" | ||
| 162 | |||
| 163 | echo "Starting container registry..." | ||
| 164 | echo " URL: http://$REGISTRY_URL" | ||
| 165 | echo " Storage: $REGISTRY_STORAGE" | ||
| 166 | echo " Config: $REGISTRY_CONFIG" | ||
| 167 | |||
| 168 | nohup "$REGISTRY_BIN" serve "$REGISTRY_CONFIG" > "$LOG_FILE" 2>&1 & | ||
| 169 | echo $! > "$PID_FILE" | ||
| 170 | sleep 2 | ||
| 171 | |||
| 172 | if kill -0 "$(cat $PID_FILE)" 2>/dev/null; then | ||
| 173 | echo "Registry started (PID: $(cat $PID_FILE))" | ||
| 174 | echo "Logs: $LOG_FILE" | ||
| 175 | else | ||
| 176 | echo "Failed to start registry. Check $LOG_FILE" | ||
| 177 | cat "$LOG_FILE" | ||
| 178 | return 1 | ||
| 179 | fi | ||
| 180 | }} | ||
| 181 | |||
| 182 | cmd_stop() {{ | ||
| 183 | if [ ! -f "$PID_FILE" ]; then | ||
| 184 | echo "Registry not running" | ||
| 185 | return 0 | ||
| 186 | fi | ||
| 187 | |||
| 188 | local pid=$(cat "$PID_FILE") | ||
| 189 | if kill -0 "$pid" 2>/dev/null; then | ||
| 190 | echo "Stopping registry (PID: $pid)..." | ||
| 191 | kill "$pid" | ||
| 192 | rm -f "$PID_FILE" | ||
| 193 | echo "Registry stopped" | ||
| 194 | else | ||
| 195 | rm -f "$PID_FILE" | ||
| 196 | echo "Registry not running (stale PID file removed)" | ||
| 197 | fi | ||
| 198 | }} | ||
| 199 | |||
| 200 | cmd_status() {{ | ||
| 201 | if [ -f "$PID_FILE" ] && kill -0 "$(cat $PID_FILE)" 2>/dev/null; then | ||
| 202 | echo "Registry running (PID: $(cat $PID_FILE))" | ||
| 203 | echo "URL: http://$REGISTRY_URL" | ||
| 204 | if curl -s "http://$REGISTRY_URL/v2/" >/dev/null 2>&1; then | ||
| 205 | echo "Status: healthy" | ||
| 206 | else | ||
| 207 | echo "Status: not responding" | ||
| 208 | fi | ||
| 209 | else | ||
| 210 | echo "Registry not running" | ||
| 211 | return 1 | ||
| 212 | fi | ||
| 213 | }} | ||
| 214 | |||
| 215 | cmd_push() {{ | ||
| 216 | if ! curl -s "http://$REGISTRY_URL/v2/" >/dev/null 2>&1; then | ||
| 217 | echo "Registry not responding at http://$REGISTRY_URL" | ||
| 218 | echo "Start it first: $0 start" | ||
| 219 | return 1 | ||
| 220 | fi | ||
| 221 | |||
| 222 | echo "Pushing OCI images from $DEPLOY_DIR_IMAGE" | ||
| 223 | echo "To registry: $REGISTRY_URL/$REGISTRY_NAMESPACE/" | ||
| 224 | echo "" | ||
| 225 | |||
| 226 | for oci_dir in "$DEPLOY_DIR_IMAGE"/*-oci; do | ||
| 227 | [ -d "$oci_dir" ] || continue | ||
| 228 | [ -f "$oci_dir/index.json" ] || continue | ||
| 229 | |||
| 230 | name=$(basename "$oci_dir" | sed 's/-latest-oci$//' | sed 's/-oci$//') | ||
| 231 | # Remove machine suffix | ||
| 232 | name=$(echo "$name" | sed 's/-qemux86-64//' | sed 's/-qemuarm64//') | ||
| 233 | # Remove rootfs timestamp | ||
| 234 | name=$(echo "$name" | sed 's/\\.rootfs-[0-9]*//') | ||
| 235 | |||
| 236 | echo "Pushing: $name" | ||
| 237 | "$SKOPEO_BIN" copy --dest-tls-verify=false \\ | ||
| 238 | "oci:$oci_dir" \\ | ||
| 239 | "docker://$REGISTRY_URL/$REGISTRY_NAMESPACE/$name:latest" | ||
| 240 | done | ||
| 241 | |||
| 242 | echo "" | ||
| 243 | echo "Done. Catalog:" | ||
| 244 | cmd_catalog | ||
| 245 | }} | ||
| 246 | |||
| 247 | cmd_catalog() {{ | ||
| 248 | curl -s "http://$REGISTRY_URL/v2/_catalog" | python3 -m json.tool 2>/dev/null || \\ | ||
| 249 | curl -s "http://$REGISTRY_URL/v2/_catalog" | ||
| 250 | }} | ||
| 251 | |||
| 252 | cmd_tags() {{ | ||
| 253 | local image="${{2:-}}" | ||
| 254 | |||
| 255 | if [ -z "$image" ]; then | ||
| 256 | echo "Usage: $0 tags <image>" | ||
| 257 | echo "" | ||
| 258 | echo "Examples:" | ||
| 259 | echo " $0 tags alpine" | ||
| 260 | echo " $0 tags yocto/container-base" | ||
| 261 | return 1 | ||
| 262 | fi | ||
| 263 | |||
| 264 | # Add namespace if not already qualified | ||
| 265 | if ! echo "$image" | grep -q '/'; then | ||
| 266 | image="$REGISTRY_NAMESPACE/$image" | ||
| 267 | fi | ||
| 268 | |||
| 269 | local result=$(curl -s "http://$REGISTRY_URL/v2/$image/tags/list") | ||
| 270 | |||
| 271 | # Check for errors or empty result | ||
| 272 | if [ -z "$result" ]; then | ||
| 273 | echo "Image not found: $image" | ||
| 274 | return 1 | ||
| 275 | fi | ||
| 276 | |||
| 277 | if echo "$result" | grep -qE '"errors"|NAME_UNKNOWN|MANIFEST_UNKNOWN'; then | ||
| 278 | echo "Image not found: $image" | ||
| 279 | return 1 | ||
| 280 | fi | ||
| 281 | |||
| 282 | # Check if tags array is null or empty | ||
| 283 | if echo "$result" | python3 -c "import sys,json; d=json.load(sys.stdin); exit(0 if d.get('tags') else 1)" 2>/dev/null; then | ||
| 284 | echo "$result" | python3 -m json.tool 2>/dev/null || echo "$result" | ||
| 285 | else | ||
| 286 | echo "Image not found: $image" | ||
| 287 | return 1 | ||
| 288 | fi | ||
| 289 | }} | ||
| 290 | |||
| 291 | cmd_list() {{ | ||
| 292 | if ! curl -s "http://$REGISTRY_URL/v2/" >/dev/null 2>&1; then | ||
| 293 | echo "Registry not responding at http://$REGISTRY_URL" | ||
| 294 | return 1 | ||
| 295 | fi | ||
| 296 | |||
| 297 | echo "Images in $REGISTRY_URL:" | ||
| 298 | echo "" | ||
| 299 | |||
| 300 | local repos=$(curl -s "http://$REGISTRY_URL/v2/_catalog" | python3 -c "import sys,json; print('\\n'.join(json.load(sys.stdin).get('repositories',[])))" 2>/dev/null) | ||
| 301 | |||
| 302 | if [ -z "$repos" ]; then | ||
| 303 | echo " (none)" | ||
| 304 | return 0 | ||
| 305 | fi | ||
| 306 | |||
| 307 | for repo in $repos; do | ||
| 308 | local tags=$(curl -s "http://$REGISTRY_URL/v2/$repo/tags/list" | python3 -c "import sys,json; print(' '.join(json.load(sys.stdin).get('tags',[])))" 2>/dev/null) | ||
| 309 | if [ -n "$tags" ]; then | ||
| 310 | echo " $repo: $tags" | ||
| 311 | else | ||
| 312 | echo " $repo: (no tags)" | ||
| 313 | fi | ||
| 314 | done | ||
| 315 | }} | ||
| 316 | |||
| 317 | cmd_import() {{ | ||
| 318 | local source="${{2:-}}" | ||
| 319 | local dest_name="${{3:-}}" | ||
| 320 | |||
| 321 | if [ -z "$source" ]; then | ||
| 322 | echo "Usage: $0 import <source-image> [local-name]" | ||
| 323 | echo "" | ||
| 324 | echo "Examples:" | ||
| 325 | echo " $0 import docker.io/library/alpine:latest" | ||
| 326 | echo " $0 import docker.io/library/alpine:latest my-alpine" | ||
| 327 | echo " $0 import quay.io/podman/hello:latest hello" | ||
| 328 | echo " $0 import ghcr.io/owner/image:tag" | ||
| 329 | return 1 | ||
| 330 | fi | ||
| 331 | |||
| 332 | if ! curl -s "http://$REGISTRY_URL/v2/" >/dev/null 2>&1; then | ||
| 333 | echo "Registry not responding at http://$REGISTRY_URL" | ||
| 334 | echo "Start it first: $0 start" | ||
| 335 | return 1 | ||
| 336 | fi | ||
| 337 | |||
| 338 | # Extract image name if not provided | ||
| 339 | if [ -z "$dest_name" ]; then | ||
| 340 | # docker.io/library/alpine:latest -> alpine | ||
| 341 | # quay.io/podman/hello:latest -> hello | ||
| 342 | dest_name=$(echo "$source" | rev | cut -d'/' -f1 | rev | cut -d':' -f1) | ||
| 343 | fi | ||
| 344 | |||
| 345 | # Extract tag from source, default to latest | ||
| 346 | local tag="latest" | ||
| 347 | if echo "$source" | grep -q ':'; then | ||
| 348 | tag=$(echo "$source" | rev | cut -d':' -f1 | rev) | ||
| 349 | fi | ||
| 350 | |||
| 351 | echo "Importing: $source" | ||
| 352 | echo " To: $REGISTRY_URL/$REGISTRY_NAMESPACE/$dest_name:$tag" | ||
| 353 | echo "" | ||
| 354 | |||
| 355 | "$SKOPEO_BIN" copy \\ | ||
| 356 | --dest-tls-verify=false \\ | ||
| 357 | "docker://$source" \\ | ||
| 358 | "docker://$REGISTRY_URL/$REGISTRY_NAMESPACE/$dest_name:$tag" | ||
| 359 | |||
| 360 | echo "" | ||
| 361 | echo "Import complete. Pull with:" | ||
| 362 | echo " vdkr --registry $REGISTRY_URL/$REGISTRY_NAMESPACE pull $dest_name" | ||
| 363 | echo " # or configure: vdkr vconfig registry $REGISTRY_URL/$REGISTRY_NAMESPACE" | ||
| 364 | echo " # then: vdkr pull $dest_name" | ||
| 365 | }} | ||
| 366 | |||
| 367 | cmd_help() {{ | ||
| 368 | echo "Usage: $0 <command>" | ||
| 369 | echo "" | ||
| 370 | echo "Commands:" | ||
| 371 | echo " start Start the container registry server" | ||
| 372 | echo " stop Stop the container registry server" | ||
| 373 | echo " status Check if registry is running" | ||
| 374 | echo " push Push all OCI images to registry" | ||
| 375 | echo " import <image> [name] Import 3rd party image to registry" | ||
| 376 | echo " list List all images with tags" | ||
| 377 | echo " tags <image> List tags for an image" | ||
| 378 | echo " catalog List image names (raw API)" | ||
| 379 | echo " help Show this help" | ||
| 380 | echo "" | ||
| 381 | echo "Examples:" | ||
| 382 | echo " $0 start" | ||
| 383 | echo " $0 push" | ||
| 384 | echo " $0 import docker.io/library/alpine:latest" | ||
| 385 | echo " $0 import docker.io/library/busybox:latest my-busybox" | ||
| 386 | echo " $0 list" | ||
| 387 | echo " $0 tags container-base" | ||
| 388 | echo "" | ||
| 389 | echo "Configuration:" | ||
| 390 | echo " Registry URL: $REGISTRY_URL" | ||
| 391 | echo " Namespace: $REGISTRY_NAMESPACE" | ||
| 392 | echo " Storage: $REGISTRY_STORAGE" | ||
| 393 | echo " Deploy images: $DEPLOY_DIR_IMAGE" | ||
| 394 | }} | ||
| 395 | |||
| 396 | case "${{1:-help}}" in | ||
| 397 | start) cmd_start ;; | ||
| 398 | stop) cmd_stop ;; | ||
| 399 | status) cmd_status ;; | ||
| 400 | push) cmd_push ;; | ||
| 401 | import) cmd_import "$@" ;; | ||
| 402 | list) cmd_list ;; | ||
| 403 | tags) cmd_tags "$@" ;; | ||
| 404 | catalog) cmd_catalog ;; | ||
| 405 | help|--help|-h) cmd_help ;; | ||
| 406 | *) echo "Unknown command: $1"; cmd_help; exit 1 ;; | ||
| 407 | esac | ||
| 408 | ''' | ||
| 409 | |||
| 410 | with open(script_path, 'w') as f: | ||
| 411 | f.write(script) | ||
| 412 | |||
| 413 | # Make executable | ||
| 414 | os.chmod(script_path, os.stat(script_path).st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH) | ||
| 415 | |||
| 416 | bb.plain("") | ||
| 417 | bb.plain("=" * 70) | ||
| 418 | bb.plain("Generated container registry helper script:") | ||
| 419 | bb.plain(f" {script_path}") | ||
| 420 | bb.plain("") | ||
| 421 | bb.plain("Usage:") | ||
| 422 | bb.plain(f" {script_path} start # Start registry server") | ||
| 423 | bb.plain(f" {script_path} push # Push OCI images to registry") | ||
| 424 | bb.plain(f" {script_path} catalog # List images in registry") | ||
| 425 | bb.plain(f" {script_path} stop # Stop registry server") | ||
| 426 | bb.plain("=" * 70) | ||
| 427 | bb.plain("") | ||
| 428 | } | ||
| 429 | |||
| 430 | do_generate_registry_script[depends] += "docker-distribution-native:do_populate_sysroot skopeo-native:do_populate_sysroot" | ||
| 431 | addtask do_generate_registry_script | ||
| 432 | |||
| 433 | EXCLUDE_FROM_WORLD = "1" | ||
