diff options
| author | Bruce Ashfield <bruce.ashfield@gmail.com> | 2026-01-12 16:09:12 +0000 |
|---|---|---|
| committer | Bruce Ashfield <bruce.ashfield@gmail.com> | 2026-01-21 18:00:26 -0500 |
| commit | 485e35da38150388b86dc7d86840368d2fca1bfa (patch) | |
| tree | 14a418016c3428bbd524ab089368a1b55e4f2e08 | |
| parent | 20b59031dd03283d464802f5d820539dfd54c77a (diff) | |
| download | meta-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>
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 | ||
| 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 | ||
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 |
| 51 | VIRTUALIZATION_PROFILE ?= "default" | 51 | VIRTUALIZATION_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" | ||
| 57 | IMAGE_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. |
| 55 | INHERIT += "sanity-meta-virt" | 61 | INHERIT += "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 | |||
| 3 | Local 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 | ||
| 9 | CONTAINER_REGISTRY_URL = "localhost:5000" | ||
| 10 | CONTAINER_REGISTRY_NAMESPACE = "yocto" | ||
| 11 | CONTAINER_REGISTRY_INSECURE = "1" | ||
| 12 | |||
| 13 | # 2. Generate the helper script | ||
| 14 | bitbake 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) | ||
| 24 | vdkr vconfig registry 10.0.2.2:5000/yocto | ||
| 25 | vdkr pull container-base | ||
| 26 | ``` | ||
| 27 | |||
| 28 | ## Helper Script Commands | ||
| 29 | |||
| 30 | Script 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) | ||
| 47 | CONTAINER_REGISTRY_URL = "localhost:5000" | ||
| 48 | |||
| 49 | # Image namespace | ||
| 50 | CONTAINER_REGISTRY_NAMESPACE = "yocto" | ||
| 51 | |||
| 52 | # Mark as insecure (HTTP) | ||
| 53 | CONTAINER_REGISTRY_INSECURE = "1" | ||
| 54 | |||
| 55 | # For Docker targets | ||
| 56 | DOCKER_REGISTRY_INSECURE = "localhost:5000" | ||
| 57 | |||
| 58 | # Persistent storage (default: ${TOPDIR}/container-registry) | ||
| 59 | CONTAINER_REGISTRY_STORAGE = "/data/container-registry" | ||
| 60 | ``` | ||
| 61 | |||
| 62 | ## vdkr Registry Usage | ||
| 63 | |||
| 64 | ### Pull Behavior with Registry Fallback | ||
| 65 | |||
| 66 | When a registry is configured, vdkr uses **registry-first, Docker Hub fallback** for pulls: | ||
| 67 | |||
| 68 | 1. Try configured registry first (e.g., `10.0.2.2:5000/yocto/alpine`) | ||
| 69 | 2. If not found, fall back to Docker Hub (`docker.io/library/alpine`) | ||
| 70 | |||
| 71 | This allows you to override images with local builds while still pulling public images normally. | ||
| 72 | |||
| 73 | ```bash | ||
| 74 | # One-off | ||
| 75 | vdkr --registry 10.0.2.2:5000/yocto pull alpine | ||
| 76 | |||
| 77 | # Persistent config | ||
| 78 | vdkr vconfig registry 10.0.2.2:5000/yocto | ||
| 79 | vdkr pull alpine # Tries registry first, falls back to Docker Hub | ||
| 80 | vdkr pull container-base # Pulls from registry (your Yocto-built image) | ||
| 81 | vdkr run alpine echo hello | ||
| 82 | |||
| 83 | # Clear config | ||
| 84 | vdkr vconfig registry --reset | ||
| 85 | |||
| 86 | # Image management (all commands use registry prefix for stored images) | ||
| 87 | vdkr image ls | ||
| 88 | vdkr image inspect alpine # Works for both registry and Docker Hub images | ||
| 89 | vdkr image rm <image> | ||
| 90 | vdkr image rm e7b39c54cdec # Image IDs work without transformation | ||
| 91 | ``` | ||
| 92 | |||
| 93 | ### Registry Transform | ||
| 94 | |||
| 95 | When 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 | |||
| 102 | Use `IMAGE_FEATURES` to auto-select the right package based on `CONTAINER_PROFILE`: | ||
| 103 | |||
| 104 | ```bitbake | ||
| 105 | # In local.conf | ||
| 106 | CONTAINER_REGISTRY_URL = "localhost:5000" | ||
| 107 | CONTAINER_REGISTRY_INSECURE = "1" | ||
| 108 | DOCKER_REGISTRY_INSECURE = "localhost:5000" | ||
| 109 | |||
| 110 | # Enable the feature | ||
| 111 | IMAGE_FEATURES:append = " container-registry" | ||
| 112 | ``` | ||
| 113 | |||
| 114 | This 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 | |||
| 130 | Registry 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 | |||
| 137 | For 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 | |||
| 38 | SUMMARY = "Configure custom container registry for Podman/Skopeo/Buildah (opt-in)" | ||
| 39 | DESCRIPTION = "Adds drop-in configuration for Podman, Skopeo, Buildah, and CRI-O. \ | ||
| 40 | NOT for Docker (use docker-registry-config for Docker). \ | ||
| 41 | Does NOT modify existing registries.conf - creates a separate file in \ | ||
| 42 | registries.conf.d/ that is merged at runtime. Public registries remain accessible. \ | ||
| 43 | This recipe is opt-in: requires CONTAINER_REGISTRY_URL to be set. \ | ||
| 44 | Use IMAGE_FEATURES container-registry to auto-select based on container engine." | ||
| 45 | |||
| 46 | LICENSE = "MIT" | ||
| 47 | LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/MIT;md5=0835ade698e0bcf8506ecda2f7b4f302" | ||
| 48 | |||
| 49 | # User MUST set these - recipe skips otherwise | ||
| 50 | CONTAINER_REGISTRY_URL ?= "" | ||
| 51 | CONTAINER_REGISTRY_INSECURE ?= "0" | ||
| 52 | CONTAINER_REGISTRY_SEARCH_FIRST ?= "1" | ||
| 53 | |||
| 54 | inherit allarch | ||
| 55 | |||
| 56 | # Skip recipe entirely if not configured | ||
| 57 | # User must explicitly set CONTAINER_REGISTRY_URL to enable | ||
| 58 | python() { | ||
| 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 | |||
| 64 | python 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 | |||
| 106 | FILES:${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 | ||
| 110 | RRECOMMENDS:${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 | |||
| 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" | ||
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 | |||
| 30 | SUMMARY = "Push container images to registry" | ||
| 31 | DESCRIPTION = "Discovers OCI images in the deploy directory and pushes them \ | ||
| 32 | to the configured container registry using skopeo. Works with docker-distribution, \ | ||
| 33 | Docker Hub, or any OCI-compliant registry." | ||
| 34 | |||
| 35 | LICENSE = "MIT" | ||
| 36 | LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/MIT;md5=0835ade698e0bcf8506ecda2f7b4f302" | ||
| 37 | |||
| 38 | inherit container-registry | ||
| 39 | |||
| 40 | # Additional dependencies | ||
| 41 | DEPENDS += "docker-distribution-native" | ||
| 42 | |||
| 43 | # Specific images to push (empty = auto-discover all) | ||
| 44 | CONTAINER_REGISTRY_IMAGES ?= "" | ||
| 45 | |||
| 46 | # Work directory | ||
| 47 | S = "${WORKDIR}/sources" | ||
| 48 | |||
| 49 | do_unpack[noexec] = "1" | ||
| 50 | do_patch[noexec] = "1" | ||
| 51 | do_configure[noexec] = "1" | ||
| 52 | do_compile[noexec] = "1" | ||
| 53 | do_install[noexec] = "1" | ||
| 54 | |||
| 55 | python 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 | ||
| 103 | addtask populate_registry after do_prepare_recipe_sysroot before do_build | ||
| 104 | |||
| 105 | # Allow network access for pushing to registry | ||
| 106 | do_populate_registry[network] = "1" | ||
| 107 | |||
| 108 | # Don't cache - always push fresh | ||
| 109 | do_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 | |||
| 35 | SUMMARY = "Configure insecure container registries for Docker daemon (opt-in)" | ||
| 36 | DESCRIPTION = "Creates /etc/docker/daemon.json with insecure-registries config. \ | ||
| 37 | FOR DOCKER ONLY - not for Podman/Skopeo (use container-oci-registry-config for those). \ | ||
| 38 | This recipe is opt-in: requires DOCKER_REGISTRY_INSECURE to be set. \ | ||
| 39 | Use IMAGE_FEATURES container-registry to auto-select based on container engine." | ||
| 40 | |||
| 41 | LICENSE = "MIT" | ||
| 42 | LIC_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" | ||
| 46 | DOCKER_REGISTRY_INSECURE ?= "" | ||
| 47 | |||
| 48 | inherit allarch | ||
| 49 | |||
| 50 | # Skip recipe entirely if not configured | ||
| 51 | python() { | ||
| 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 | |||
| 57 | python 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 | |||
| 81 | FILES:${PN} = "${sysconfdir}/docker/daemon.json" | ||
| 82 | |||
| 83 | # Docker must be installed for this to be useful | ||
| 84 | RDEPENDS:${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 | |||
| 24 | version: 0.1 | ||
| 25 | |||
| 26 | log: | ||
| 27 | level: info | ||
| 28 | formatter: text | ||
| 29 | fields: | ||
| 30 | service: container-registry | ||
| 31 | |||
| 32 | storage: | ||
| 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 | |||
| 47 | http: | ||
| 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 | ||
| 57 | health: | ||
| 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 | |||
| 36 | set -e | ||
| 37 | |||
| 38 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" | ||
| 39 | PID_FILE="/tmp/container-registry.pid" | ||
| 40 | LOG_FILE="/tmp/container-registry.log" | ||
| 41 | |||
| 42 | # Default configuration | ||
| 43 | REGISTRY_PORT="${REGISTRY_PORT:-5000}" | ||
| 44 | REGISTRY_STORAGE="${REGISTRY_STORAGE:-/tmp/container-registry}" | ||
| 45 | |||
| 46 | # Find registry binary | ||
| 47 | find_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 | ||
| 76 | find_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 | |||
| 99 | cmd_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 | |||
| 157 | cmd_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 | |||
| 180 | cmd_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 | |||
| 211 | cmd_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 | |||
| 220 | cmd_help() { | ||
| 221 | cat << EOF | ||
| 222 | Usage: $(basename "$0") <command> [options] | ||
| 223 | |||
| 224 | Commands: | ||
| 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 | |||
| 231 | Environment: | ||
| 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 | |||
| 238 | Examples: | ||
| 239 | $(basename "$0") start | ||
| 240 | $(basename "$0") start /path/to/config.yml | ||
| 241 | $(basename "$0") status | ||
| 242 | $(basename "$0") stop | ||
| 243 | EOF | ||
| 244 | } | ||
| 245 | |||
| 246 | # Main | ||
| 247 | case "${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 | ;; | ||
| 268 | esac | ||
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 |
| 85 | IMAGE_FEATURES[validitems] += "virt-unique-hostname" | 85 | IMAGE_FEATURES[validitems] += "virt-unique-hostname" |
| 86 | IMAGE_FEATURES[validitems] += "container-tools" | 86 | IMAGE_FEATURES[validitems] += "container-tools" |
| 87 | IMAGE_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 | ||
| 91 | FEATURE_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 | ||
| 88 | IMAGE_FEATURES += "ssh-server-openssh" | 96 | IMAGE_FEATURES += "ssh-server-openssh" |
| 89 | IMAGE_FEATURES += "package-management" | 97 | IMAGE_FEATURES += "package-management" |
