summaryrefslogtreecommitdiffstats
path: root/recipes-containers/container-registry
diff options
context:
space:
mode:
authorBruce Ashfield <bruce.ashfield@gmail.com>2026-01-12 21:12:03 +0000
committerBruce Ashfield <bruce.ashfield@gmail.com>2026-02-09 03:32:52 +0000
commit68320b2c0a6751bf54ae9376d6e1e1dab30c0376 (patch)
tree88c9e017796b496dc34099a6ee1df5b123b3f229 /recipes-containers/container-registry
parent8b19fa53399cdeb18b1cdd41276ecac5a4f659b0 (diff)
downloadmeta-virtualization-68320b2c0a6751bf54ae9376d6e1e1dab30c0376.tar.gz
container-registry: add management commands and documentation
Registry management commands: - delete <image>:<tag>: Remove tagged images from registry - gc: Garbage collection with dry-run preview and confirmation - push <image> --tag: Explicit tags now require image name (prevents accidentally tagging all images with same version) Config improvements: - Copy config to storage directory with baked-in storage path - Fixes gc which reads config directly (not via env var) - All registry files now in ${TOPDIR}/container-registry/ Documentation: - Development Loop workflow (build, push, pull, test) - Build-time OCI labels (revision, branch, created) - Complete command reference Signed-off-by: Bruce Ashfield <bruce.ashfield@gmail.com>
Diffstat (limited to 'recipes-containers/container-registry')
-rw-r--r--recipes-containers/container-registry/README.md68
-rw-r--r--recipes-containers/container-registry/container-registry-index.bb218
-rw-r--r--recipes-containers/container-registry/files/container-registry-dev.yml3
3 files changed, 272 insertions, 17 deletions
diff --git a/recipes-containers/container-registry/README.md b/recipes-containers/container-registry/README.md
index 82932706..1a0f74eb 100644
--- a/recipes-containers/container-registry/README.md
+++ b/recipes-containers/container-registry/README.md
@@ -34,8 +34,10 @@ Script location: `${TOPDIR}/container-registry/container-registry.sh` (outside t
34| `start` | Start the container registry server | 34| `start` | Start the container registry server |
35| `stop` | Stop the container registry server | 35| `stop` | Stop the container registry server |
36| `status` | Check if registry is running | 36| `status` | Check if registry is running |
37| `push [options]` | Push all OCI images from deploy/ to registry | 37| `push [image] [options]` | Push OCI images from deploy/ to registry |
38| `import <image> [name]` | Import 3rd party image to registry | 38| `import <image> [name]` | Import 3rd party image to registry |
39| `delete <image>:<tag>` | Delete a tagged image from registry |
40| `gc` | Garbage collect unreferenced blobs |
39| `list` | List all images with their tags | 41| `list` | List all images with their tags |
40| `tags <image>` | List tags for a specific image | 42| `tags <image>` | List tags for a specific image |
41| `catalog` | Raw API catalog output | 43| `catalog` | Raw API catalog output |
@@ -43,9 +45,9 @@ Script location: `${TOPDIR}/container-registry/container-registry.sh` (outside t
43### Push Options 45### Push Options
44 46
45```bash 47```bash
46# Explicit tags 48# Explicit tags (require image name)
47container-registry.sh push --tag v1.0.0 49container-registry.sh push container-base --tag v1.0.0
48container-registry.sh push --tag latest --tag v1.0.0 50container-registry.sh push container-base --tag latest --tag v1.0.0
49 51
50# Strategy-based (see Tag Strategies below) 52# Strategy-based (see Tag Strategies below)
51container-registry.sh push --strategy "sha branch latest" 53container-registry.sh push --strategy "sha branch latest"
@@ -89,6 +91,64 @@ Result: `my-app:1.2.3`, `my-app:1.2`, `my-app:1`, `my-app:latest`
89IMAGE_VERSION=1.2.3 container-registry.sh push --strategy "semver sha latest" 91IMAGE_VERSION=1.2.3 container-registry.sh push --strategy "semver sha latest"
90``` 92```
91 93
94## Development Loop
95
96The default strategy (`timestamp latest`) supports a simple development workflow:
97
98```bash
99# Build
100bitbake container-base
101
102# Push (creates both timestamp tag AND :latest)
103./container-registry/container-registry.sh push
104
105# Pull on target - :latest is implicit, gets your most recent push
106vdkr pull container-base
107
108# Test
109vdkr run container-base /bin/sh
110
111# Repeat: rebuild, push, pull - no tag hunting needed
112```
113
114Each push overwrites `:latest` with your newest build. The timestamp tags (`20260112-143022`) remain for rollback/debugging.
115
116## Build-Time OCI Labels
117
118Container images automatically include standard OCI traceability labels:
119
120```bash
121$ skopeo inspect oci:container-base-oci | jq '.Labels'
122{
123 "org.opencontainers.image.revision": "8a3f2b1",
124 "org.opencontainers.image.ref.name": "master",
125 "org.opencontainers.image.created": "2026-01-12T20:32:24Z"
126}
127```
128
129| Label | Source | Description |
130|-------|--------|-------------|
131| `org.opencontainers.image.revision` | git SHA from TOPDIR | Code traceability |
132| `org.opencontainers.image.ref.name` | git branch from TOPDIR | Branch tracking |
133| `org.opencontainers.image.created` | Build timestamp | When image was built |
134| `org.opencontainers.image.version` | PV (if set) | Semantic version |
135
136### Customizing Labels
137
138```bitbake
139# In local.conf or image recipe
140
141# Explicit override (e.g., from CI/CD)
142OCI_IMAGE_REVISION = "${CI_COMMIT_SHA}"
143OCI_IMAGE_BRANCH = "${CI_BRANCH}"
144
145# Disable specific label
146OCI_IMAGE_REVISION = "none"
147
148# Disable all auto-labels
149OCI_IMAGE_AUTO_LABELS = "0"
150```
151
92## Configuration (local.conf) 152## Configuration (local.conf)
93 153
94```bitbake 154```bitbake
diff --git a/recipes-containers/container-registry/container-registry-index.bb b/recipes-containers/container-registry/container-registry-index.bb
index f865bfaa..75090faa 100644
--- a/recipes-containers/container-registry/container-registry-index.bb
+++ b/recipes-containers/container-registry/container-registry-index.bb
@@ -93,6 +93,7 @@ CONTAINER_REGISTRY_SCRIPT = "${CONTAINER_REGISTRY_STORAGE}/container-registry.sh
93python do_generate_registry_script() { 93python do_generate_registry_script() {
94 import os 94 import os
95 import stat 95 import stat
96 import shutil
96 97
97 script_path = d.getVar('CONTAINER_REGISTRY_SCRIPT') 98 script_path = d.getVar('CONTAINER_REGISTRY_SCRIPT')
98 deploy_dir = d.getVar('DEPLOY_DIR') 99 deploy_dir = d.getVar('DEPLOY_DIR')
@@ -105,9 +106,6 @@ python do_generate_registry_script() {
105 # Find skopeo binary path 106 # Find skopeo binary path
106 skopeo_bin = os.path.join(d.getVar('STAGING_SBINDIR_NATIVE') or '', 'skopeo') 107 skopeo_bin = os.path.join(d.getVar('STAGING_SBINDIR_NATIVE') or '', 'skopeo')
107 108
108 # Config file path
109 config_file = os.path.join(d.getVar('THISDIR'), 'files', 'container-registry-dev.yml')
110
111 # Registry settings 109 # Registry settings
112 registry_url = d.getVar('CONTAINER_REGISTRY_URL') 110 registry_url = d.getVar('CONTAINER_REGISTRY_URL')
113 registry_namespace = d.getVar('CONTAINER_REGISTRY_NAMESPACE') 111 registry_namespace = d.getVar('CONTAINER_REGISTRY_NAMESPACE')
@@ -115,8 +113,23 @@ python do_generate_registry_script() {
115 tag_strategy = d.getVar('CONTAINER_REGISTRY_TAG_STRATEGY') or 'latest' 113 tag_strategy = d.getVar('CONTAINER_REGISTRY_TAG_STRATEGY') or 'latest'
116 target_arch = d.getVar('TARGET_ARCH') or '' 114 target_arch = d.getVar('TARGET_ARCH') or ''
117 115
116 # Create storage directory
117 os.makedirs(registry_storage, exist_ok=True)
118 os.makedirs(deploy_dir, exist_ok=True) 118 os.makedirs(deploy_dir, exist_ok=True)
119 119
120 # Copy config file to storage directory and update storage path
121 src_config = os.path.join(d.getVar('THISDIR'), 'files', 'container-registry-dev.yml')
122 config_file = os.path.join(registry_storage, 'registry-config.yml')
123 with open(src_config, 'r') as f:
124 config_content = f.read()
125 # Replace the default storage path with actual path
126 config_content = config_content.replace(
127 'rootdirectory: /tmp/container-registry',
128 f'rootdirectory: {registry_storage}'
129 )
130 with open(config_file, 'w') as f:
131 f.write(config_content)
132
120 script = f'''#!/bin/bash 133 script = f'''#!/bin/bash
121# Container Registry Helper Script 134# Container Registry Helper Script
122# Generated by: bitbake container-registry-index -c generate_registry_script 135# Generated by: bitbake container-registry-index -c generate_registry_script
@@ -233,7 +246,6 @@ cmd_start() {{
233 fi 246 fi
234 247
235 mkdir -p "$REGISTRY_STORAGE" 248 mkdir -p "$REGISTRY_STORAGE"
236 export REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY="$REGISTRY_STORAGE"
237 249
238 echo "Starting container registry..." 250 echo "Starting container registry..."
239 echo " URL: http://$REGISTRY_URL" 251 echo " URL: http://$REGISTRY_URL"
@@ -290,10 +302,11 @@ cmd_status() {{
290cmd_push() {{ 302cmd_push() {{
291 shift # Remove 'push' from args 303 shift # Remove 'push' from args
292 304
293 # Parse options 305 # Parse options and positional args
294 local explicit_tags="" 306 local explicit_tags=""
295 local strategy="${{CONTAINER_REGISTRY_TAG_STRATEGY:-$DEFAULT_TAG_STRATEGY}}" 307 local strategy="${{CONTAINER_REGISTRY_TAG_STRATEGY:-$DEFAULT_TAG_STRATEGY}}"
296 local version="${{IMAGE_VERSION:-}}" 308 local version="${{IMAGE_VERSION:-}}"
309 local image_filter=""
297 310
298 while [ $# -gt 0 ]; do 311 while [ $# -gt 0 ]; do
299 case "$1" in 312 case "$1" in
@@ -309,12 +322,34 @@ cmd_push() {{
309 version="$2" 322 version="$2"
310 shift 2 323 shift 2
311 ;; 324 ;;
325 -*)
326 echo "Unknown option: $1"
327 return 1
328 ;;
312 *) 329 *)
330 # Positional arg = image name filter
331 if [ -z "$image_filter" ]; then
332 image_filter="$1"
333 fi
313 shift 334 shift
314 ;; 335 ;;
315 esac 336 esac
316 done 337 done
317 338
339 # Explicit tags require an image name
340 if [ -n "$explicit_tags" ] && [ -z "$image_filter" ]; then
341 echo "Error: --tag requires an image name"
342 echo "Usage: $0 push <image> --tag <tag>"
343 echo ""
344 echo "Examples:"
345 echo " $0 push container-base --tag v1.0.0"
346 echo " $0 push container-base --tag latest --tag v1.0.0"
347 echo ""
348 echo "To push all images, use a strategy instead:"
349 echo " $0 push --strategy 'timestamp latest'"
350 return 1
351 fi
352
318 # Export version for generate_tags 353 # Export version for generate_tags
319 export IMAGE_VERSION="$version" 354 export IMAGE_VERSION="$version"
320 355
@@ -332,11 +367,16 @@ cmd_push() {{
332 tags=$(generate_tags "$strategy") 367 tags=$(generate_tags "$strategy")
333 fi 368 fi
334 369
335 echo "Pushing OCI images from $DEPLOY_DIR_IMAGE" 370 if [ -n "$image_filter" ]; then
371 echo "Pushing image: $image_filter"
372 else
373 echo "Pushing all OCI images from $DEPLOY_DIR_IMAGE"
374 fi
336 echo "To registry: $REGISTRY_URL/$REGISTRY_NAMESPACE/" 375 echo "To registry: $REGISTRY_URL/$REGISTRY_NAMESPACE/"
337 echo "Tags: $tags" 376 echo "Tags: $tags"
338 echo "" 377 echo ""
339 378
379 local found=0
340 for oci_dir in "$DEPLOY_DIR_IMAGE"/*-oci; do 380 for oci_dir in "$DEPLOY_DIR_IMAGE"/*-oci; do
341 [ -d "$oci_dir" ] || continue 381 [ -d "$oci_dir" ] || continue
342 [ -f "$oci_dir/index.json" ] || continue 382 [ -f "$oci_dir/index.json" ] || continue
@@ -347,6 +387,20 @@ cmd_push() {{
347 # Remove rootfs timestamp 387 # Remove rootfs timestamp
348 name=$(echo "$name" | sed 's/\\.rootfs-[0-9]*//') 388 name=$(echo "$name" | sed 's/\\.rootfs-[0-9]*//')
349 389
390 # Filter by image name if specified
391 if [ -n "$image_filter" ]; then
392 # Match exact name or name.rootfs variant
393 case "$name" in
394 "$image_filter"|"$image_filter.rootfs")
395 : # match
396 ;;
397 *)
398 continue
399 ;;
400 esac
401 fi
402
403 found=1
350 echo "Pushing: $name" 404 echo "Pushing: $name"
351 for tag in $tags; do 405 for tag in $tags; do
352 echo " -> $REGISTRY_URL/$REGISTRY_NAMESPACE/$name:$tag" 406 echo " -> $REGISTRY_URL/$REGISTRY_NAMESPACE/$name:$tag"
@@ -356,6 +410,19 @@ cmd_push() {{
356 done 410 done
357 done 411 done
358 412
413 if [ -n "$image_filter" ] && [ "$found" = "0" ]; then
414 echo "Error: Image '$image_filter' not found in $DEPLOY_DIR_IMAGE"
415 echo ""
416 echo "Available images:"
417 for oci_dir in "$DEPLOY_DIR_IMAGE"/*-oci; do
418 [ -d "$oci_dir" ] || continue
419 [ -f "$oci_dir/index.json" ] || continue
420 n=$(basename "$oci_dir" | sed 's/-latest-oci$//' | sed 's/-oci$//' | sed 's/-qemux86-64//' | sed 's/-qemuarm64//' | sed 's/\\.rootfs-[0-9]*//')
421 echo " $n"
422 done
423 return 1
424 fi
425
359 echo "" 426 echo ""
360 echo "Done. Catalog:" 427 echo "Done. Catalog:"
361 cmd_catalog 428 cmd_catalog
@@ -481,6 +548,124 @@ cmd_import() {{
481 echo " # then: vdkr pull $dest_name" 548 echo " # then: vdkr pull $dest_name"
482}} 549}}
483 550
551cmd_delete() {{
552 local image="${{2:-}}"
553
554 if [ -z "$image" ]; then
555 echo "Usage: $0 delete <image>[:<tag>]"
556 echo ""
557 echo "Examples:"
558 echo " $0 delete container-base:v1.0.0 # Delete specific tag"
559 echo " $0 delete container-base:20260112-143022"
560 echo " $0 delete yocto/alpine:latest # With namespace"
561 echo ""
562 echo "Note: Deleting a tag removes the manifest reference."
563 echo "Run garbage collection to reclaim disk space."
564 return 1
565 fi
566
567 if ! curl -s "http://$REGISTRY_URL/v2/" >/dev/null 2>&1; then
568 echo "Registry not responding at http://$REGISTRY_URL"
569 return 1
570 fi
571
572 # Parse image:tag
573 local name tag
574 if echo "$image" | grep -q ':'; then
575 name=$(echo "$image" | rev | cut -d':' -f2- | rev)
576 tag=$(echo "$image" | rev | cut -d':' -f1 | rev)
577 else
578 echo "Error: Tag required. Use format: <image>:<tag>"
579 echo "Example: $0 delete container-base:v1.0.0"
580 return 1
581 fi
582
583 # Add namespace if not already qualified
584 if ! echo "$name" | grep -q '/'; then
585 name="$REGISTRY_NAMESPACE/$name"
586 fi
587
588 echo "Deleting: $name:$tag"
589
590 # Get the digest for the tag (try OCI format first, then Docker V2)
591 local digest=""
592 for accept in "application/vnd.oci.image.manifest.v1+json" \
593 "application/vnd.docker.distribution.manifest.v2+json"; do
594 digest=$(curl -s -I -H "Accept: $accept" \
595 "http://$REGISTRY_URL/v2/$name/manifests/$tag" 2>/dev/null \
596 | grep -i "docker-content-digest" | awk '{{print $2}}' | tr -d '\r\n')
597 [ -n "$digest" ] && break
598 done
599
600 if [ -z "$digest" ]; then
601 echo "Error: Tag not found: $name:$tag"
602 return 1
603 fi
604
605 echo " Digest: $digest"
606
607 # Delete by digest
608 local status=$(curl -s -o /dev/null -w "%{{http_code}}" -X DELETE \
609 "http://$REGISTRY_URL/v2/$name/manifests/$digest")
610
611 if [ "$status" = "202" ]; then
612 echo " Deleted successfully"
613 echo ""
614 echo "Note: Run garbage collection to reclaim disk space:"
615 echo " $0 gc"
616 elif [ "$status" = "405" ]; then
617 echo "Error: Deletion not enabled in registry config"
618 echo "Add 'storage.delete.enabled: true' to registry config and restart"
619 return 1
620 else
621 echo "Error: Delete failed (HTTP $status)"
622 return 1
623 fi
624}}
625
626cmd_gc() {{
627 echo "Running garbage collection..."
628 echo ""
629
630 if [ ! -x "$REGISTRY_BIN" ]; then
631 echo "Error: Registry binary not found at $REGISTRY_BIN"
632 echo "Build it with: bitbake docker-distribution-native"
633 return 1
634 fi
635
636 # Check if registry is running
637 local was_running=0
638 if [ -f "$PID_FILE" ] && kill -0 "$(cat $PID_FILE)" 2>/dev/null; then
639 was_running=1
640 echo "Stopping registry for garbage collection..."
641 cmd_stop
642 sleep 1
643 fi
644
645 echo "Collecting garbage from: $REGISTRY_STORAGE"
646 echo ""
647
648 # Run garbage collection (dry-run first to show what would be deleted)
649 "$REGISTRY_BIN" garbage-collect --dry-run "$REGISTRY_CONFIG" 2>&1 || true
650 echo ""
651
652 read -p "Proceed with garbage collection? [y/N] " confirm
653 if [ "$confirm" = "y" ] || [ "$confirm" = "Y" ]; then
654 "$REGISTRY_BIN" garbage-collect "$REGISTRY_CONFIG"
655 echo ""
656 echo "Garbage collection complete."
657 else
658 echo "Cancelled."
659 fi
660
661 # Restart if it was running
662 if [ "$was_running" = "1" ]; then
663 echo ""
664 echo "Restarting registry..."
665 cmd_start
666 fi
667}}
668
484cmd_help() {{ 669cmd_help() {{
485 echo "Usage: $0 <command> [options]" 670 echo "Usage: $0 <command> [options]"
486 echo "" 671 echo ""
@@ -488,15 +673,18 @@ cmd_help() {{
488 echo " start Start the container registry server" 673 echo " start Start the container registry server"
489 echo " stop Stop the container registry server" 674 echo " stop Stop the container registry server"
490 echo " status Check if registry is running" 675 echo " status Check if registry is running"
491 echo " push [options] Push all OCI images to registry" 676 echo " push [image] [opts] Push OCI images to registry"
492 echo " import <image> [name] Import 3rd party image to registry" 677 echo " import <image> [name] Import 3rd party image to registry"
678 echo " delete <image>:<tag> Delete a tagged image from registry"
679 echo " gc Garbage collect unreferenced blobs"
493 echo " list List all images with tags" 680 echo " list List all images with tags"
494 echo " tags <image> List tags for an image" 681 echo " tags <image> List tags for an image"
495 echo " catalog List image names (raw API)" 682 echo " catalog List image names (raw API)"
496 echo " help Show this help" 683 echo " help Show this help"
497 echo "" 684 echo ""
498 echo "Push options:" 685 echo "Push options:"
499 echo " --tag, -t <tag> Explicit tag (can be repeated for multiple tags)" 686 echo " <image> Image name (required when using --tag)"
687 echo " --tag, -t <tag> Explicit tag (can be repeated, requires image name)"
500 echo " --strategy, -s <str> Tag strategy (default: $DEFAULT_TAG_STRATEGY)" 688 echo " --strategy, -s <str> Tag strategy (default: $DEFAULT_TAG_STRATEGY)"
501 echo " --version, -v <ver> Version for semver strategy (e.g., 1.2.3)" 689 echo " --version, -v <ver> Version for semver strategy (e.g., 1.2.3)"
502 echo "" 690 echo ""
@@ -511,13 +699,15 @@ cmd_help() {{
511 echo "" 699 echo ""
512 echo "Examples:" 700 echo "Examples:"
513 echo " $0 start" 701 echo " $0 start"
514 echo " $0 push # Uses default strategy" 702 echo " $0 push # Push all, default strategy"
515 echo " $0 push --tag v1.0.0 # Explicit tag" 703 echo " $0 push container-base # Push one, default strategy"
516 echo " $0 push --tag latest --tag v1.0.0 # Multiple tags" 704 echo " $0 push container-base --tag v1.0.0 # Explicit tag (one image)"
517 echo " $0 push --strategy 'sha branch latest' # Strategy-based" 705 echo " $0 push container-base -t latest -t v1.0.0 # Multiple explicit tags"
518 echo " $0 push --strategy semver --version 1.2.3 # SemVer tags" 706 echo " $0 push --strategy 'sha branch latest' # All images, strategy"
707 echo " $0 push --strategy semver --version 1.2.3 # All images, SemVer"
519 echo " $0 import docker.io/library/alpine:latest" 708 echo " $0 import docker.io/library/alpine:latest"
520 echo " $0 import docker.io/library/busybox:latest my-busybox" 709 echo " $0 import docker.io/library/busybox:latest my-busybox"
710 echo " $0 delete container-base:20260112-143022"
521 echo " $0 list" 711 echo " $0 list"
522 echo " $0 tags container-base" 712 echo " $0 tags container-base"
523 echo "" 713 echo ""
@@ -541,6 +731,8 @@ case "${{1:-help}}" in
541 status) cmd_status ;; 731 status) cmd_status ;;
542 push) cmd_push "$@" ;; 732 push) cmd_push "$@" ;;
543 import) cmd_import "$@" ;; 733 import) cmd_import "$@" ;;
734 delete) cmd_delete "$@" ;;
735 gc) cmd_gc ;;
544 list) cmd_list ;; 736 list) cmd_list ;;
545 tags) cmd_tags "$@" ;; 737 tags) cmd_tags "$@" ;;
546 catalog) cmd_catalog ;; 738 catalog) cmd_catalog ;;
diff --git a/recipes-containers/container-registry/files/container-registry-dev.yml b/recipes-containers/container-registry/files/container-registry-dev.yml
index ed0a7c88..80e5296c 100644
--- a/recipes-containers/container-registry/files/container-registry-dev.yml
+++ b/recipes-containers/container-registry/files/container-registry-dev.yml
@@ -33,6 +33,9 @@ storage:
33 filesystem: 33 filesystem:
34 # Storage directory - override with REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY env var 34 # Storage directory - override with REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY env var
35 rootdirectory: /tmp/container-registry 35 rootdirectory: /tmp/container-registry
36 # Enable deletion of images/tags
37 delete:
38 enabled: true
36 # Don't redirect to external storage 39 # Don't redirect to external storage
37 redirect: 40 redirect:
38 disable: true 41 disable: true