summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorBruce Ashfield <bruce.ashfield@gmail.com>2026-01-12 16:09:12 +0000
committerBruce Ashfield <bruce.ashfield@gmail.com>2026-01-21 18:00:26 -0500
commit485e35da38150388b86dc7d86840368d2fca1bfa (patch)
tree14a418016c3428bbd524ab089368a1b55e4f2e08
parent20b59031dd03283d464802f5d820539dfd54c77a (diff)
downloadmeta-virtualization-485e35da38150388b86dc7d86840368d2fca1bfa.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>
-rw-r--r--classes/container-registry.bbclass203
-rw-r--r--conf/layer.conf6
-rw-r--r--recipes-containers/container-registry/README.md140
-rw-r--r--recipes-containers/container-registry/container-oci-registry-config.bb110
-rw-r--r--recipes-containers/container-registry/container-registry-index.bb433
-rw-r--r--recipes-containers/container-registry/container-registry-populate.bb109
-rw-r--r--recipes-containers/container-registry/docker-registry-config.bb84
-rw-r--r--recipes-containers/container-registry/files/container-registry-dev.yml61
-rw-r--r--recipes-containers/container-registry/files/container-registry.sh268
-rw-r--r--recipes-extended/images/container-image-host.bb8
10 files changed, 1422 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
30CONTAINER_REGISTRY_URL ?= "localhost:5000"
31CONTAINER_REGISTRY_NAMESPACE ?= "yocto"
32CONTAINER_REGISTRY_TLS_VERIFY ?= "false"
33CONTAINER_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"
39CONTAINER_REGISTRY_STORAGE ?= "${TOPDIR}/container-registry"
40
41# Require skopeo-native for registry operations
42DEPENDS += "skopeo-native"
43
44def 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
96def 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
153def 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
diff --git a/conf/layer.conf b/conf/layer.conf
index 5ec45ee0..85694344 100644
--- a/conf/layer.conf
+++ b/conf/layer.conf
@@ -50,6 +50,12 @@ CONTAINER_PROFILE ?= "default"
50# virt profile can be: kvm, xen, runx 50# virt profile can be: kvm, xen, runx
51VIRTUALIZATION_PROFILE ?= "default" 51VIRTUALIZATION_PROFILE ?= "default"
52 52
53# Custom IMAGE_FEATURES for container images
54# container-registry: Install registry config based on container engine
55# Requires: CONTAINER_REGISTRY_URL (for OCI) or DOCKER_REGISTRY_INSECURE (for Docker)
56# Usage: IMAGE_FEATURES:append = " container-registry"
57IMAGE_FEATURES[validitems] += "container-registry"
58
53# Sanity check for meta-virtualization layer. 59# Sanity check for meta-virtualization layer.
54# Setting SKIP_META_VIRT_SANITY_CHECK to "1" would skip the bbappend files check. 60# Setting SKIP_META_VIRT_SANITY_CHECK to "1" would skip the bbappend files check.
55INHERIT += "sanity-meta-virt" 61INHERIT += "sanity-meta-virt"
diff --git a/recipes-containers/container-registry/README.md b/recipes-containers/container-registry/README.md
new file mode 100644
index 00000000..11db39bb
--- /dev/null
+++ b/recipes-containers/container-registry/README.md
@@ -0,0 +1,140 @@
1# Container Registry Infrastructure
2
3Local container registry for Yocto/OE builds - analogous to package-index for containers.
4
5## Quick Start
6
7```bash
8# 1. Configure in local.conf
9CONTAINER_REGISTRY_URL = "localhost:5000"
10CONTAINER_REGISTRY_NAMESPACE = "yocto"
11CONTAINER_REGISTRY_INSECURE = "1"
12
13# 2. Generate the helper script
14bitbake container-registry-index -c generate_registry_script
15
16# 3. Start registry, push images
17$TOPDIR/container-registry/container-registry.sh start
18$TOPDIR/container-registry/container-registry.sh push
19
20# 4. Import 3rd party images
21$TOPDIR/container-registry/container-registry.sh import docker.io/library/alpine:latest
22
23# 5. Use with vdkr (10.0.2.2 is QEMU slirp gateway to localhost)
24vdkr vconfig registry 10.0.2.2:5000/yocto
25vdkr pull container-base
26```
27
28## Helper Script Commands
29
30Script location: `${TOPDIR}/container-registry/container-registry.sh` (outside tmp/, persists)
31
32| Command | Description |
33|---------|-------------|
34| `start` | Start the container registry server |
35| `stop` | Stop the container registry server |
36| `status` | Check if registry is running |
37| `push` | Push all OCI images from deploy/ to registry |
38| `import <image> [name]` | Import 3rd party image to registry |
39| `list` | List all images with their tags |
40| `tags <image>` | List tags for a specific image |
41| `catalog` | Raw API catalog output |
42
43## Configuration (local.conf)
44
45```bitbake
46# Registry endpoint (host-side)
47CONTAINER_REGISTRY_URL = "localhost:5000"
48
49# Image namespace
50CONTAINER_REGISTRY_NAMESPACE = "yocto"
51
52# Mark as insecure (HTTP)
53CONTAINER_REGISTRY_INSECURE = "1"
54
55# For Docker targets
56DOCKER_REGISTRY_INSECURE = "localhost:5000"
57
58# Persistent storage (default: ${TOPDIR}/container-registry)
59CONTAINER_REGISTRY_STORAGE = "/data/container-registry"
60```
61
62## vdkr Registry Usage
63
64### Pull Behavior with Registry Fallback
65
66When a registry is configured, vdkr uses **registry-first, Docker Hub fallback** for pulls:
67
681. Try configured registry first (e.g., `10.0.2.2:5000/yocto/alpine`)
692. If not found, fall back to Docker Hub (`docker.io/library/alpine`)
70
71This allows you to override images with local builds while still pulling public images normally.
72
73```bash
74# One-off
75vdkr --registry 10.0.2.2:5000/yocto pull alpine
76
77# Persistent config
78vdkr vconfig registry 10.0.2.2:5000/yocto
79vdkr pull alpine # Tries registry first, falls back to Docker Hub
80vdkr pull container-base # Pulls from registry (your Yocto-built image)
81vdkr run alpine echo hello
82
83# Clear config
84vdkr vconfig registry --reset
85
86# Image management (all commands use registry prefix for stored images)
87vdkr image ls
88vdkr image inspect alpine # Works for both registry and Docker Hub images
89vdkr image rm <image>
90vdkr image rm e7b39c54cdec # Image IDs work without transformation
91```
92
93### Registry Transform
94
95When a registry is configured:
96- `pull`, `run` - Use fallback (registry first, then Docker Hub)
97- `inspect`, `history`, `rmi`, `tag`, `images` - No transform (use actual local image names)
98- Image IDs (hex strings like `e7b39c54cdec`) - Never transformed
99
100## Baking Registry Config into Target Images
101
102Use `IMAGE_FEATURES` to auto-select the right package based on `CONTAINER_PROFILE`:
103
104```bitbake
105# In local.conf
106CONTAINER_REGISTRY_URL = "localhost:5000"
107CONTAINER_REGISTRY_INSECURE = "1"
108DOCKER_REGISTRY_INSECURE = "localhost:5000"
109
110# Enable the feature
111IMAGE_FEATURES:append = " container-registry"
112```
113
114This installs:
115- **Docker profile** → `docker-registry-config` → `/etc/docker/daemon.json`
116- **Podman profile** → `container-oci-registry-config` → `/etc/containers/registries.conf.d/`
117
118## Files
119
120| File | Description |
121|------|-------------|
122| `container-registry-index.bb` | Generates helper script with baked-in paths |
123| `container-registry-populate.bb` | Alternative bitbake-driven push |
124| `container-oci-registry-config.bb` | OCI tools config (Podman/Skopeo/Buildah/CRI-O) |
125| `docker-registry-config.bb` | Docker daemon config |
126| `files/container-registry-dev.yml` | Development registry config |
127
128## Storage
129
130Registry data and script are stored at `${TOPDIR}/container-registry/` by default:
131- Outside tmp/, persists across builds and cleanall
132- Imported and pushed images are copied here
133- Script regenerates with same paths after tmp/ cleanup
134
135## Localhost to 10.0.2.2 Translation
136
137For vdkr baked configs, `localhost` URLs are auto-translated to `10.0.2.2` (QEMU slirp gateway):
138- Set `CONTAINER_REGISTRY_URL = "localhost:5000"` in local.conf
139- Host-side operations use localhost directly
140- vdkr inside QEMU accesses via 10.0.2.2 automatically
diff --git a/recipes-containers/container-registry/container-oci-registry-config.bb b/recipes-containers/container-registry/container-oci-registry-config.bb
new file mode 100644
index 00000000..ee6760f4
--- /dev/null
+++ b/recipes-containers/container-registry/container-oci-registry-config.bb
@@ -0,0 +1,110 @@
1# SPDX-FileCopyrightText: Copyright (C) 2025 Bruce Ashfield
2#
3# SPDX-License-Identifier: MIT
4#
5# container-oci-registry-config.bb
6# ===========================================================================
7# Configure custom container registry for OCI runtimes (OPT-IN)
8# ===========================================================================
9#
10# FOR OCI-COMPATIBLE RUNTIMES (use /etc/containers/registries.conf.d/):
11# - Podman
12# - Skopeo
13# - Buildah
14# - CRI-O
15#
16# NOT FOR DOCKER - Docker uses /etc/docker/daemon.json
17# See: docker-registry-config.bb for Docker configuration
18#
19# This recipe creates a drop-in configuration file for accessing a custom
20# container registry. It is completely OPT-IN and does not modify any
21# existing configuration files.
22#
23# IMPORTANT: This recipe:
24# - Does NOT modify docker-distribution or container-host-config
25# - Does NOT install automatically - user must add to IMAGE_INSTALL
26# - Does NOT clobber public registry access (docker.io, quay.io, etc.)
27# - Uses drop-in files in /etc/containers/registries.conf.d/
28# - Skips entirely if CONTAINER_REGISTRY_URL is not set
29#
30# Usage:
31# # In local.conf or image recipe - BOTH required:
32# CONTAINER_REGISTRY_URL = "localhost:5000"
33# CONTAINER_REGISTRY_INSECURE = "1"
34# IMAGE_INSTALL:append = " container-oci-registry-config"
35#
36# ===========================================================================
37
38SUMMARY = "Configure custom container registry for Podman/Skopeo/Buildah (opt-in)"
39DESCRIPTION = "Adds drop-in configuration for Podman, Skopeo, Buildah, and CRI-O. \
40NOT for Docker (use docker-registry-config for Docker). \
41Does NOT modify existing registries.conf - creates a separate file in \
42registries.conf.d/ that is merged at runtime. Public registries remain accessible. \
43This recipe is opt-in: requires CONTAINER_REGISTRY_URL to be set. \
44Use IMAGE_FEATURES container-registry to auto-select based on container engine."
45
46LICENSE = "MIT"
47LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/MIT;md5=0835ade698e0bcf8506ecda2f7b4f302"
48
49# User MUST set these - recipe skips otherwise
50CONTAINER_REGISTRY_URL ?= ""
51CONTAINER_REGISTRY_INSECURE ?= "0"
52CONTAINER_REGISTRY_SEARCH_FIRST ?= "1"
53
54inherit allarch
55
56# Skip recipe entirely if not configured
57# User must explicitly set CONTAINER_REGISTRY_URL to enable
58python() {
59 registry = d.getVar('CONTAINER_REGISTRY_URL')
60 if not registry:
61 raise bb.parse.SkipRecipe("CONTAINER_REGISTRY_URL not set - recipe is opt-in only")
62}
63
64python do_install() {
65 import os
66
67 registry = d.getVar('CONTAINER_REGISTRY_URL')
68 insecure = d.getVar('CONTAINER_REGISTRY_INSECURE') == "1"
69 search_first = d.getVar('CONTAINER_REGISTRY_SEARCH_FIRST') == "1"
70
71 dest = d.getVar('D')
72 confdir = os.path.join(dest, d.getVar('sysconfdir').lstrip('/'),
73 'containers', 'registries.conf.d')
74 os.makedirs(confdir, exist_ok=True)
75
76 # Generate drop-in config
77 # Filename starts with 50- so it's processed after base config but
78 # can be overridden by higher-numbered files
79 config_path = os.path.join(confdir, '50-custom-registry.conf')
80
81 with open(config_path, 'w') as f:
82 f.write(f"# Custom container registry: {registry}\n")
83 f.write(f"# Generated by container-registry-config recipe\n")
84 f.write(f"# This is ADDITIVE - base registries.conf is unchanged\n")
85 f.write(f"# Public registries (docker.io, quay.io) remain accessible\n")
86 f.write(f"#\n")
87 f.write(f"# To remove: uninstall container-registry-config package\n")
88 f.write(f"# or delete this file\n\n")
89
90 if search_first:
91 # Add to unqualified-search-registries
92 # This means short names like "myapp:latest" will search here first
93 f.write(f"# Search this registry for unqualified image names\n")
94 f.write(f'unqualified-search-registries = ["{registry}"]\n\n')
95
96 if insecure:
97 # Mark registry as insecure (HTTP or self-signed TLS)
98 f.write(f"# Registry uses HTTP or has untrusted TLS certificate\n")
99 f.write(f'[[registry]]\n')
100 f.write(f'location = "{registry}"\n')
101 f.write(f'insecure = true\n')
102
103 bb.note(f"Created registry config for {registry} (insecure={insecure})")
104}
105
106FILES:${PN} = "${sysconfdir}/containers/registries.conf.d"
107
108# Soft dependency - works with or without container-host-config
109# If container-host-config is installed, our drop-in extends it
110RRECOMMENDS:${PN} = "container-host-config"
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
30SUMMARY = "Populate container registry with OCI images"
31LICENSE = "MIT"
32
33INHIBIT_DEFAULT_DEPS = "1"
34PACKAGES = ""
35
36inherit nopackages container-registry
37
38deltask do_fetch
39deltask do_unpack
40deltask do_patch
41deltask do_configure
42deltask do_compile
43deltask do_install
44deltask do_populate_lic
45deltask do_populate_sysroot
46
47do_container_registry_index[nostamp] = "1"
48do_container_registry_index[network] = "1"
49do_container_registry_index[depends] += "skopeo-native:do_populate_sysroot"
50
51python 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
87addtask 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
91CONTAINER_REGISTRY_SCRIPT = "${CONTAINER_REGISTRY_STORAGE}/container-registry.sh"
92
93python 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
134set -e
135
136# Pre-configured paths from bitbake
137REGISTRY_BIN="{registry_bin}"
138SKOPEO_BIN="{skopeo_bin}"
139REGISTRY_CONFIG="{config_file}"
140REGISTRY_STORAGE="{registry_storage}"
141REGISTRY_URL="{registry_url}"
142REGISTRY_NAMESPACE="{registry_namespace}"
143DEPLOY_DIR_IMAGE="{deploy_dir_image}"
144
145PID_FILE="/tmp/container-registry.pid"
146LOG_FILE="/tmp/container-registry.log"
147
148cmd_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
182cmd_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
200cmd_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
215cmd_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
247cmd_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
252cmd_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
291cmd_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
317cmd_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
367cmd_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
396case "${{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 ;;
407esac
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
430do_generate_registry_script[depends] += "docker-distribution-native:do_populate_sysroot skopeo-native:do_populate_sysroot"
431addtask do_generate_registry_script
432
433EXCLUDE_FROM_WORLD = "1"
diff --git a/recipes-containers/container-registry/container-registry-populate.bb b/recipes-containers/container-registry/container-registry-populate.bb
new file mode 100644
index 00000000..d44b1051
--- /dev/null
+++ b/recipes-containers/container-registry/container-registry-populate.bb
@@ -0,0 +1,109 @@
1# SPDX-FileCopyrightText: Copyright (C) 2025 Bruce Ashfield
2#
3# SPDX-License-Identifier: MIT
4#
5# container-registry-populate.bb
6# ===========================================================================
7# Push OCI container images from deploy directory to a container registry
8# ===========================================================================
9#
10# This recipe discovers OCI images in DEPLOY_DIR_IMAGE and pushes them
11# to the configured container registry using skopeo.
12#
13# Usage:
14# # Set registry URL (default: localhost:5000)
15# CONTAINER_REGISTRY_URL = "localhost:5000"
16#
17# # Push all discovered images
18# bitbake container-registry-populate
19#
20# # Push specific images only
21# CONTAINER_REGISTRY_IMAGES = "container-base container-app"
22# bitbake container-registry-populate
23#
24# Prerequisites:
25# - docker-distribution-native built and running
26# - Container images built (bitbake container-base)
27#
28# ===========================================================================
29
30SUMMARY = "Push container images to registry"
31DESCRIPTION = "Discovers OCI images in the deploy directory and pushes them \
32to the configured container registry using skopeo. Works with docker-distribution, \
33Docker Hub, or any OCI-compliant registry."
34
35LICENSE = "MIT"
36LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/MIT;md5=0835ade698e0bcf8506ecda2f7b4f302"
37
38inherit container-registry
39
40# Additional dependencies
41DEPENDS += "docker-distribution-native"
42
43# Specific images to push (empty = auto-discover all)
44CONTAINER_REGISTRY_IMAGES ?= ""
45
46# Work directory
47S = "${WORKDIR}/sources"
48
49do_unpack[noexec] = "1"
50do_patch[noexec] = "1"
51do_configure[noexec] = "1"
52do_compile[noexec] = "1"
53do_install[noexec] = "1"
54
55python do_populate_registry() {
56 """Push OCI images to the configured registry."""
57 import os
58
59 registry = d.getVar('CONTAINER_REGISTRY_URL')
60 namespace = d.getVar('CONTAINER_REGISTRY_NAMESPACE')
61 specific_images = (d.getVar('CONTAINER_REGISTRY_IMAGES') or '').split()
62
63 bb.note(f"Container Registry: {registry}/{namespace}/")
64 bb.note(f"Tag Strategy: {d.getVar('CONTAINER_REGISTRY_TAG_STRATEGY')}")
65
66 # Discover OCI images
67 all_images = container_registry_discover_oci_images(d)
68
69 if not all_images:
70 bb.warn("No OCI images found in deploy directory")
71 bb.note(f"Deploy directory: {d.getVar('DEPLOY_DIR_IMAGE')}")
72 bb.note("Build container images first: bitbake container-base")
73 return
74
75 bb.note(f"Discovered {len(all_images)} OCI images")
76
77 # Filter if specific images requested
78 if specific_images:
79 images = [(path, name) for path, name in all_images if name in specific_images]
80 if not images:
81 bb.warn(f"None of the requested images found: {specific_images}")
82 bb.note(f"Available images: {[name for _, name in all_images]}")
83 return
84 else:
85 images = all_images
86
87 # Push each image
88 pushed_refs = []
89 for oci_path, image_name in images:
90 bb.note(f"Processing: {image_name} from {oci_path}")
91 refs = container_registry_push(d, oci_path, image_name)
92 pushed_refs.extend(refs)
93
94 # Summary
95 bb.note("=" * 60)
96 bb.note(f"Pushed {len(pushed_refs)} image references:")
97 for ref in pushed_refs:
98 bb.note(f" {ref}")
99 bb.note("=" * 60)
100}
101
102# Run after prepare_recipe_sysroot so skopeo-native is available
103addtask populate_registry after do_prepare_recipe_sysroot before do_build
104
105# Allow network access for pushing to registry
106do_populate_registry[network] = "1"
107
108# Don't cache - always push fresh
109do_populate_registry[nostamp] = "1"
diff --git a/recipes-containers/container-registry/docker-registry-config.bb b/recipes-containers/container-registry/docker-registry-config.bb
new file mode 100644
index 00000000..eee74c98
--- /dev/null
+++ b/recipes-containers/container-registry/docker-registry-config.bb
@@ -0,0 +1,84 @@
1# SPDX-FileCopyrightText: Copyright (C) 2025 Bruce Ashfield
2#
3# SPDX-License-Identifier: MIT
4#
5# docker-registry-config.bb
6# ===========================================================================
7# Configure custom container registry for Docker daemon (OPT-IN)
8# ===========================================================================
9#
10# FOR DOCKER ONLY - creates /etc/docker/daemon.json
11#
12# NOT for Podman/Skopeo/Buildah - they use /etc/containers/registries.conf.d/
13# See: container-registry-config.bb for Podman/Skopeo/Buildah
14#
15# This recipe creates daemon.json for Docker to access insecure registries.
16# It is completely OPT-IN and requires explicit configuration.
17#
18# NOTE: Docker does not support "default registry" like our vdkr transform.
19# Users must still use fully qualified image names unless using Docker Hub.
20# This config only handles insecure registry trust.
21#
22# IMPORTANT: This recipe:
23# - Does NOT install automatically - user must add to IMAGE_INSTALL
24# - Skips entirely if DOCKER_REGISTRY_INSECURE is not set
25# - Creates /etc/docker/daemon.json (will be merged if docker recipe
26# also creates one, or may need RCONFLICTS handling)
27#
28# Usage:
29# # In local.conf or image recipe:
30# DOCKER_REGISTRY_INSECURE = "10.0.2.2:5000 myregistry.local:5000"
31# IMAGE_INSTALL:append = " docker-registry-config"
32#
33# ===========================================================================
34
35SUMMARY = "Configure insecure container registries for Docker daemon (opt-in)"
36DESCRIPTION = "Creates /etc/docker/daemon.json with insecure-registries config. \
37FOR DOCKER ONLY - not for Podman/Skopeo (use container-oci-registry-config for those). \
38This recipe is opt-in: requires DOCKER_REGISTRY_INSECURE to be set. \
39Use IMAGE_FEATURES container-registry to auto-select based on container engine."
40
41LICENSE = "MIT"
42LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/MIT;md5=0835ade698e0bcf8506ecda2f7b4f302"
43
44# Space-separated list of insecure registries
45# Example: "10.0.2.2:5000 myregistry.local:5000"
46DOCKER_REGISTRY_INSECURE ?= ""
47
48inherit allarch
49
50# Skip recipe entirely if not configured
51python() {
52 registries = d.getVar('DOCKER_REGISTRY_INSECURE')
53 if not registries or not registries.strip():
54 raise bb.parse.SkipRecipe("DOCKER_REGISTRY_INSECURE not set - recipe is opt-in only")
55}
56
57python do_install() {
58 import os
59 import json
60
61 registries = d.getVar('DOCKER_REGISTRY_INSECURE').split()
62
63 dest = d.getVar('D')
64 confdir = os.path.join(dest, d.getVar('sysconfdir').lstrip('/'), 'docker')
65 os.makedirs(confdir, exist_ok=True)
66
67 config_path = os.path.join(confdir, 'daemon.json')
68
69 # Create daemon.json
70 config = {
71 "insecure-registries": registries
72 }
73
74 with open(config_path, 'w') as f:
75 json.dump(config, f, indent=2)
76 f.write("\n")
77
78 bb.note(f"Created Docker config with insecure registries: {registries}")
79}
80
81FILES:${PN} = "${sysconfdir}/docker/daemon.json"
82
83# Docker must be installed for this to be useful
84RDEPENDS:${PN} = "docker"
diff --git a/recipes-containers/container-registry/files/container-registry-dev.yml b/recipes-containers/container-registry/files/container-registry-dev.yml
new file mode 100644
index 00000000..ed0a7c88
--- /dev/null
+++ b/recipes-containers/container-registry/files/container-registry-dev.yml
@@ -0,0 +1,61 @@
1# Container Registry Development Configuration
2# ============================================
3#
4# This is a simple configuration for running a local container registry
5# for development purposes. It uses filesystem storage and listens on
6# port 5000 without TLS.
7#
8# Usage:
9# oe-run-native docker-distribution-native registry serve \
10# /path/to/container-registry-dev.yml
11#
12# Or with explicit paths:
13# /path/to/sysroot-native/usr/sbin/registry serve \
14# /path/to/container-registry-dev.yml
15#
16# For production, consider:
17# - Enabling TLS
18# - Adding authentication
19# - Using cloud storage (S3, GCS, Azure)
20# - Setting up garbage collection
21#
22# See: https://distribution.github.io/distribution/about/configuration/
23
24version: 0.1
25
26log:
27 level: info
28 formatter: text
29 fields:
30 service: container-registry
31
32storage:
33 filesystem:
34 # Storage directory - override with REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY env var
35 rootdirectory: /tmp/container-registry
36 # Don't redirect to external storage
37 redirect:
38 disable: true
39 # Maintenance settings
40 maintenance:
41 uploadpurging:
42 enabled: true
43 age: 168h # 1 week
44 interval: 24h
45 dryrun: false
46
47http:
48 addr: :5000
49 headers:
50 X-Content-Type-Options: [nosniff]
51 # For development - allow HTTP. In production, use TLS.
52 # tls:
53 # certificate: /path/to/cert.pem
54 # key: /path/to/key.pem
55
56# Health check endpoint
57health:
58 storagedriver:
59 enabled: true
60 interval: 10s
61 threshold: 3
diff --git a/recipes-containers/container-registry/files/container-registry.sh b/recipes-containers/container-registry/files/container-registry.sh
new file mode 100644
index 00000000..14684c9a
--- /dev/null
+++ b/recipes-containers/container-registry/files/container-registry.sh
@@ -0,0 +1,268 @@
1#!/bin/bash
2# SPDX-FileCopyrightText: Copyright (C) 2025 Bruce Ashfield
3# SPDX-License-Identifier: MIT
4#
5# container-registry.sh
6# ============================================
7# Helper script to start/stop a local container registry
8# ============================================
9#
10# This script manages a local docker-distribution registry server
11# for development purposes.
12#
13# Usage:
14# container-registry.sh start [config.yml] [storage-dir]
15# container-registry.sh stop
16# container-registry.sh status
17# container-registry.sh logs
18#
19# Examples:
20# # Start with defaults (port 5000, storage in /tmp/container-registry)
21# container-registry.sh start
22#
23# # Start with custom config
24# container-registry.sh start /path/to/config.yml
25#
26# # Start with custom storage
27# container-registry.sh start /path/to/config.yml /var/lib/registry
28#
29# Environment:
30# REGISTRY_BIN Path to registry binary (auto-detected from oe-run-native)
31# REGISTRY_CONFIG Path to config file
32# REGISTRY_STORAGE Storage directory
33# REGISTRY_PORT Port to listen on (default: 5000)
34#
35
36set -e
37
38SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
39PID_FILE="/tmp/container-registry.pid"
40LOG_FILE="/tmp/container-registry.log"
41
42# Default configuration
43REGISTRY_PORT="${REGISTRY_PORT:-5000}"
44REGISTRY_STORAGE="${REGISTRY_STORAGE:-/tmp/container-registry}"
45
46# Find registry binary
47find_registry_bin() {
48 # Check if provided via environment
49 if [ -n "$REGISTRY_BIN" ] && [ -x "$REGISTRY_BIN" ]; then
50 echo "$REGISTRY_BIN"
51 return 0
52 fi
53
54 # Try to find in Yocto native sysroot
55 local builddir="${BUILDDIR:-$(pwd)}"
56 local native_sysroot="$builddir/tmp/work/x86_64-linux/docker-distribution-native"
57
58 if [ -d "$native_sysroot" ]; then
59 local registry=$(find "$native_sysroot" -name "registry" -type f -executable 2>/dev/null | head -1)
60 if [ -n "$registry" ]; then
61 echo "$registry"
62 return 0
63 fi
64 fi
65
66 # Try system PATH
67 if command -v registry &>/dev/null; then
68 command -v registry
69 return 0
70 fi
71
72 return 1
73}
74
75# Find config file
76find_config() {
77 local config="$1"
78
79 if [ -n "$config" ] && [ -f "$config" ]; then
80 echo "$config"
81 return 0
82 fi
83
84 # Check environment
85 if [ -n "$REGISTRY_CONFIG" ] && [ -f "$REGISTRY_CONFIG" ]; then
86 echo "$REGISTRY_CONFIG"
87 return 0
88 fi
89
90 # Check script directory
91 if [ -f "$SCRIPT_DIR/container-registry-dev.yml" ]; then
92 echo "$SCRIPT_DIR/container-registry-dev.yml"
93 return 0
94 fi
95
96 return 1
97}
98
99cmd_start() {
100 local config="$1"
101 local storage="${2:-$REGISTRY_STORAGE}"
102
103 if [ -f "$PID_FILE" ]; then
104 local pid=$(cat "$PID_FILE")
105 if kill -0 "$pid" 2>/dev/null; then
106 echo "Registry already running (PID: $pid)"
107 return 1
108 fi
109 rm -f "$PID_FILE"
110 fi
111
112 local registry_bin
113 if ! registry_bin=$(find_registry_bin); then
114 echo "Error: Cannot find registry binary"
115 echo "Build it with: bitbake docker-distribution-native"
116 return 1
117 fi
118
119 local config_file
120 if ! config_file=$(find_config "$config"); then
121 echo "Error: Cannot find config file"
122 echo "Provide config file as argument or set REGISTRY_CONFIG"
123 return 1
124 fi
125
126 # Create storage directory
127 mkdir -p "$storage"
128
129 echo "Starting container registry..."
130 echo " Binary: $registry_bin"
131 echo " Config: $config_file"
132 echo " Storage: $storage"
133 echo " Port: $REGISTRY_PORT"
134
135 # Export storage directory for config
136 export REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY="$storage"
137
138 # Start registry in background
139 nohup "$registry_bin" serve "$config_file" > "$LOG_FILE" 2>&1 &
140 local pid=$!
141 echo "$pid" > "$PID_FILE"
142
143 # Wait for startup
144 sleep 2
145
146 if kill -0 "$pid" 2>/dev/null; then
147 echo "Registry started (PID: $pid)"
148 echo "Access at: http://localhost:$REGISTRY_PORT"
149 echo "Logs at: $LOG_FILE"
150 else
151 echo "Failed to start registry. Check logs: $LOG_FILE"
152 cat "$LOG_FILE"
153 return 1
154 fi
155}
156
157cmd_stop() {
158 if [ ! -f "$PID_FILE" ]; then
159 echo "Registry not running (no PID file)"
160 return 0
161 fi
162
163 local pid=$(cat "$PID_FILE")
164
165 if kill -0 "$pid" 2>/dev/null; then
166 echo "Stopping registry (PID: $pid)..."
167 kill "$pid"
168 sleep 2
169
170 if kill -0 "$pid" 2>/dev/null; then
171 echo "Force killing..."
172 kill -9 "$pid" 2>/dev/null || true
173 fi
174 fi
175
176 rm -f "$PID_FILE"
177 echo "Registry stopped"
178}
179
180cmd_status() {
181 if [ ! -f "$PID_FILE" ]; then
182 echo "Registry not running"
183 return 1
184 fi
185
186 local pid=$(cat "$PID_FILE")
187
188 if kill -0 "$pid" 2>/dev/null; then
189 echo "Registry running (PID: $pid)"
190 echo "Port: $REGISTRY_PORT"
191
192 # Check if responding
193 if curl -s "http://localhost:$REGISTRY_PORT/v2/" >/dev/null 2>&1; then
194 echo "Status: healthy"
195
196 # List images
197 local catalog=$(curl -s "http://localhost:$REGISTRY_PORT/v2/_catalog" 2>/dev/null)
198 if [ -n "$catalog" ]; then
199 echo "Catalog: $catalog"
200 fi
201 else
202 echo "Status: not responding"
203 fi
204 else
205 echo "Registry not running (stale PID file)"
206 rm -f "$PID_FILE"
207 return 1
208 fi
209}
210
211cmd_logs() {
212 if [ -f "$LOG_FILE" ]; then
213 tail -f "$LOG_FILE"
214 else
215 echo "No log file found"
216 return 1
217 fi
218}
219
220cmd_help() {
221 cat << EOF
222Usage: $(basename "$0") <command> [options]
223
224Commands:
225 start [config] [storage] Start the registry
226 stop Stop the registry
227 status Show registry status
228 logs Tail registry logs
229 help Show this help
230
231Environment:
232 REGISTRY_BIN Path to registry binary
233 REGISTRY_CONFIG Path to config file
234 REGISTRY_STORAGE Storage directory (default: /tmp/container-registry)
235 REGISTRY_PORT Port to listen on (default: 5000)
236 BUILDDIR Yocto build directory (for finding native binaries)
237
238Examples:
239 $(basename "$0") start
240 $(basename "$0") start /path/to/config.yml
241 $(basename "$0") status
242 $(basename "$0") stop
243EOF
244}
245
246# Main
247case "${1:-help}" in
248 start)
249 cmd_start "$2" "$3"
250 ;;
251 stop)
252 cmd_stop
253 ;;
254 status)
255 cmd_status
256 ;;
257 logs)
258 cmd_logs
259 ;;
260 help|--help|-h)
261 cmd_help
262 ;;
263 *)
264 echo "Unknown command: $1"
265 cmd_help
266 exit 1
267 ;;
268esac
diff --git a/recipes-extended/images/container-image-host.bb b/recipes-extended/images/container-image-host.bb
index 454fcd45..723f0cf5 100644
--- a/recipes-extended/images/container-image-host.bb
+++ b/recipes-extended/images/container-image-host.bb
@@ -84,6 +84,14 @@ REQUIRED_DISTRO_FEATURES:append = " ${@bb.utils.contains('VIRTUAL-RUNTIME_contai
84# of the host name to make it unique 84# of the host name to make it unique
85IMAGE_FEATURES[validitems] += "virt-unique-hostname" 85IMAGE_FEATURES[validitems] += "virt-unique-hostname"
86IMAGE_FEATURES[validitems] += "container-tools" 86IMAGE_FEATURES[validitems] += "container-tools"
87IMAGE_FEATURES[validitems] += "container-registry"
88
89# Container registry configuration packages (opt-in via IMAGE_FEATURES += "container-registry")
90# Requires CONTAINER_REGISTRY_URL and/or DOCKER_REGISTRY_INSECURE to be set
91FEATURE_PACKAGES_container-registry = "\
92 ${@bb.utils.contains_any('VIRTUAL-RUNTIME_container_engine', 'docker docker-moby', 'docker-registry-config', '', d)} \
93 ${@bb.utils.contains_any('VIRTUAL-RUNTIME_container_engine', 'podman containerd cri-o', 'container-oci-registry-config', '', d)} \
94"
87 95
88IMAGE_FEATURES += "ssh-server-openssh" 96IMAGE_FEATURES += "ssh-server-openssh"
89IMAGE_FEATURES += "package-management" 97IMAGE_FEATURES += "package-management"