summaryrefslogtreecommitdiffstats
path: root/classes
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-02-09 03:32:52 +0000
commit87ed625c043e4cdbabf569227b189823cd08db8e (patch)
treea307f96f218f3be0e2741fe13079400b24ee8487 /classes
parent33944038c68d8e497e8dd9861c5ca6c4da7d48e5 (diff)
downloadmeta-virtualization-87ed625c043e4cdbabf569227b189823cd08db8e.tar.gz
container-registry: add local OCI registry infrastructure
Add container registry support for Yocto container workflows: - container-registry.bbclass with helper functions - container-registry-index.bb generates helper script with baked paths - docker-registry-config.bb for Docker daemon on targets - container-oci-registry-config.bb for Podman/Skopeo/Buildah targets - IMAGE_FEATURES container-registry for easy target configuration Signed-off-by: Bruce Ashfield <bruce.ashfield@gmail.com>
Diffstat (limited to 'classes')
-rw-r--r--classes/container-registry.bbclass203
1 files changed, 203 insertions, 0 deletions
diff --git a/classes/container-registry.bbclass b/classes/container-registry.bbclass
new file mode 100644
index 00000000..f5a2d0c3
--- /dev/null
+++ b/classes/container-registry.bbclass
@@ -0,0 +1,203 @@
1# SPDX-FileCopyrightText: Copyright (C) 2025 Bruce Ashfield
2#
3# SPDX-License-Identifier: MIT
4#
5# container-registry.bbclass
6# ===========================================================================
7# Container registry operations for pushing OCI images to registries
8# ===========================================================================
9#
10# This class provides functions to push OCI images from the deploy directory
11# to a container registry. It works with docker-distribution, Docker Hub,
12# or any OCI-compliant registry.
13#
14# Usage:
15# inherit container-registry
16#
17# # In do_populate_registry task:
18# container_registry_push(d, oci_path, image_name)
19#
20# Configuration:
21# CONTAINER_REGISTRY_URL = "localhost:5000" # Registry endpoint
22# CONTAINER_REGISTRY_NAMESPACE = "yocto" # Image namespace
23# CONTAINER_REGISTRY_TLS_VERIFY = "false" # TLS verification
24# CONTAINER_REGISTRY_TAG_STRATEGY = "timestamp latest" # Tag generation
25# CONTAINER_REGISTRY_STORAGE = "${TOPDIR}/container-registry" # Persistent storage
26#
27# ===========================================================================
28
29# Registry configuration
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