summaryrefslogtreecommitdiffstats
path: root/recipes-containers/container-registry/container-registry-index.bb
diff options
context:
space:
mode:
authorBruce Ashfield <bruce.ashfield@gmail.com>2026-02-09 03:17:13 +0000
committerBruce Ashfield <bruce.ashfield@gmail.com>2026-02-09 03:34:12 +0000
commit092aa81983335b2346a725eebd2a75fc785bb42b (patch)
tree4cae1a055b5027c9a049004a339bc3f752cbf8f1 /recipes-containers/container-registry/container-registry-index.bb
parent4ed680e32e3670a4e50038387572ee7a35374c0e (diff)
downloadmeta-virtualization-092aa81983335b2346a725eebd2a75fc785bb42b.tar.gz
container-registry: add secure registry infrastructure with TLS and auth
Add opt-in secure registry mode with auto-generated TLS certificates and htpasswd authentication. New BitBake variables: CONTAINER_REGISTRY_SECURE - Enable TLS (HTTPS) for local registry CONTAINER_REGISTRY_AUTH - Enable htpasswd auth (requires SECURE=1) CONTAINER_REGISTRY_USERNAME/PASSWORD - Credential configuration CONTAINER_REGISTRY_CERT_DAYS/CA_DAYS - Certificate validity CONTAINER_REGISTRY_CERT_SAN - Custom SAN entries The bbclass validates conflicting settings (AUTH without SECURE) and provides credential helper functions for skopeo push operations. PKI infrastructure (CA + server cert with SAN) is auto-generated at bitbake build time via openssl-native. The generated helper script supports both TLS-only and TLS+auth modes. The script now supports environment variable overrides for CONTAINER_REGISTRY_STORAGE, CONTAINER_REGISTRY_URL, and CONTAINER_REGISTRY_NAMESPACE, uses per-port PID files to allow multiple instances, and auto-generates config files when running from an overridden storage path. Signed-off-by: Bruce Ashfield <bruce.ashfield@gmail.com>
Diffstat (limited to 'recipes-containers/container-registry/container-registry-index.bb')
-rw-r--r--recipes-containers/container-registry/container-registry-index.bb904
1 files changed, 845 insertions, 59 deletions
diff --git a/recipes-containers/container-registry/container-registry-index.bb b/recipes-containers/container-registry/container-registry-index.bb
index 29125eee..590e89b3 100644
--- a/recipes-containers/container-registry/container-registry-index.bb
+++ b/recipes-containers/container-registry/container-registry-index.bb
@@ -115,12 +115,127 @@ python do_generate_registry_script() {
115 tag_strategy = d.getVar('CONTAINER_REGISTRY_TAG_STRATEGY') or 'latest' 115 tag_strategy = d.getVar('CONTAINER_REGISTRY_TAG_STRATEGY') or 'latest'
116 target_arch = d.getVar('TARGET_ARCH') or '' 116 target_arch = d.getVar('TARGET_ARCH') or ''
117 117
118 # Secure mode settings
119 secure_mode = d.getVar('CONTAINER_REGISTRY_SECURE') or '0'
120 auth_enabled = d.getVar('CONTAINER_REGISTRY_AUTH') or '0'
121 registry_username = d.getVar('CONTAINER_REGISTRY_USERNAME') or 'yocto'
122 ca_days = d.getVar('CONTAINER_REGISTRY_CA_DAYS') or '3650'
123 cert_days = d.getVar('CONTAINER_REGISTRY_CERT_DAYS') or '365'
124 custom_san = d.getVar('CONTAINER_REGISTRY_CERT_SAN') or ''
125
118 # Create storage directory 126 # Create storage directory
119 os.makedirs(registry_storage, exist_ok=True) 127 os.makedirs(registry_storage, exist_ok=True)
120 os.makedirs(deploy_dir, exist_ok=True) 128 os.makedirs(deploy_dir, exist_ok=True)
121 129
130 # Generate PKI infrastructure for secure mode
131 if secure_mode == '1':
132 import subprocess
133
134 pki_dir = os.path.join(registry_storage, 'pki')
135 auth_dir = os.path.join(registry_storage, 'auth')
136 os.makedirs(pki_dir, exist_ok=True)
137 os.makedirs(auth_dir, exist_ok=True)
138
139 ca_key = os.path.join(pki_dir, 'ca.key')
140 ca_crt = os.path.join(pki_dir, 'ca.crt')
141 server_key = os.path.join(pki_dir, 'server.key')
142 server_csr = os.path.join(pki_dir, 'server.csr')
143 server_crt = os.path.join(pki_dir, 'server.crt')
144
145 # Find openssl from native sysroot
146 openssl_bin = os.path.join(native_sysroot, 'usr', 'bin', 'openssl')
147 if not os.path.exists(openssl_bin):
148 openssl_bin = 'openssl' # Fall back to system openssl
149
150 # Generate CA if it doesn't exist
151 if not os.path.exists(ca_crt):
152 bb.plain("Generating PKI infrastructure for secure registry...")
153
154 # Generate CA private key
155 subprocess.run([
156 openssl_bin, 'genrsa', '-out', ca_key, '4096'
157 ], check=True, capture_output=True)
158 os.chmod(ca_key, 0o600)
159
160 # Generate CA certificate
161 subprocess.run([
162 openssl_bin, 'req', '-new', '-x509', '-days', ca_days,
163 '-key', ca_key, '-out', ca_crt,
164 '-subj', '/CN=Container Registry CA/O=Yocto/C=US'
165 ], check=True, capture_output=True)
166
167 bb.plain(f" Generated CA certificate: {ca_crt}")
168
169 # Generate server cert if it doesn't exist
170 if not os.path.exists(server_crt):
171 # Build SAN list
172 registry_host = registry_url.split(':')[0] if ':' in registry_url else registry_url
173 san_entries = [
174 'DNS:localhost',
175 f'DNS:{registry_host}',
176 'IP:127.0.0.1',
177 'IP:10.0.2.2'
178 ]
179 if custom_san:
180 san_entries.extend(custom_san.split(','))
181 san_string = ','.join(san_entries)
182
183 # Generate server private key
184 subprocess.run([
185 openssl_bin, 'genrsa', '-out', server_key, '4096'
186 ], check=True, capture_output=True)
187 os.chmod(server_key, 0o600)
188
189 # Create OpenSSL config for SAN
190 openssl_conf = os.path.join(pki_dir, 'openssl.cnf')
191 with open(openssl_conf, 'w') as f:
192 f.write(f'''[req]
193distinguished_name = req_distinguished_name
194req_extensions = v3_req
195prompt = no
196
197[req_distinguished_name]
198CN = {registry_host}
199O = Yocto
200C = US
201
202[v3_req]
203basicConstraints = CA:FALSE
204keyUsage = nonRepudiation, digitalSignature, keyEncipherment
205subjectAltName = {san_string}
206
207[v3_ca]
208subjectAltName = {san_string}
209''')
210
211 # Generate CSR
212 subprocess.run([
213 openssl_bin, 'req', '-new', '-key', server_key,
214 '-out', server_csr, '-config', openssl_conf
215 ], check=True, capture_output=True)
216
217 # Sign server cert with CA
218 subprocess.run([
219 openssl_bin, 'x509', '-req', '-days', cert_days,
220 '-in', server_csr, '-CA', ca_crt, '-CAkey', ca_key,
221 '-CAcreateserial', '-out', server_crt,
222 '-extensions', 'v3_ca', '-extfile', openssl_conf
223 ], check=True, capture_output=True)
224
225 bb.plain(f" Generated server certificate with SAN: {', '.join(san_entries)}")
226
227 bb.plain(f" PKI directory: {pki_dir}")
228
122 # Copy config file to storage directory and update storage path 229 # Copy config file to storage directory and update storage path
123 src_config = os.path.join(d.getVar('THISDIR'), 'files', 'container-registry-dev.yml') 230 # Use secure config when CONTAINER_REGISTRY_SECURE=1
231 if secure_mode == '1':
232 src_config = os.path.join(d.getVar('THISDIR'), 'files', 'container-registry-secure.yml')
233 if not os.path.exists(src_config):
234 bb.warn("Secure mode enabled but container-registry-secure.yml not found, using dev config")
235 src_config = os.path.join(d.getVar('THISDIR'), 'files', 'container-registry-dev.yml')
236 else:
237 src_config = os.path.join(d.getVar('THISDIR'), 'files', 'container-registry-dev.yml')
238
124 config_file = os.path.join(registry_storage, 'registry-config.yml') 239 config_file = os.path.join(registry_storage, 'registry-config.yml')
125 with open(src_config, 'r') as f: 240 with open(src_config, 'r') as f:
126 config_content = f.read() 241 config_content = f.read()
@@ -129,6 +244,23 @@ python do_generate_registry_script() {
129 'rootdirectory: /tmp/container-registry', 244 'rootdirectory: /tmp/container-registry',
130 f'rootdirectory: {registry_storage}' 245 f'rootdirectory: {registry_storage}'
131 ) 246 )
247 config_content = config_content.replace(
248 '__STORAGE_PATH__',
249 registry_storage
250 )
251 config_content = config_content.replace(
252 '__PKI_DIR__',
253 os.path.join(registry_storage, 'pki')
254 )
255 config_content = config_content.replace(
256 '__AUTH_DIR__',
257 os.path.join(registry_storage, 'auth')
258 )
259 # Remove auth section if AUTH is not enabled (TLS-only mode)
260 if secure_mode == '1' and auth_enabled != '1':
261 import re
262 # Remove the auth block (including htpasswd subsection)
263 config_content = re.sub(r'\n# htpasswd authentication\nauth:\n htpasswd:\n realm:.*\n path:.*\n', '\n', config_content)
132 with open(config_file, 'w') as f: 264 with open(config_file, 'w') as f:
133 f.write(config_content) 265 f.write(config_content)
134 266
@@ -155,13 +287,13 @@ python do_generate_registry_script() {
155 287
156set -e 288set -e
157 289
158# Pre-configured paths from bitbake 290# Pre-configured paths from bitbake (overridable via environment)
159REGISTRY_BIN="{registry_bin}" 291REGISTRY_BIN="{registry_bin}"
160SKOPEO_BIN="{skopeo_bin}" 292SKOPEO_BIN="{skopeo_bin}"
161REGISTRY_CONFIG="{config_file}" 293REGISTRY_STORAGE="${{CONTAINER_REGISTRY_STORAGE:-{registry_storage}}}"
162REGISTRY_STORAGE="{registry_storage}" 294REGISTRY_URL="${{CONTAINER_REGISTRY_URL:-{registry_url}}}"
163REGISTRY_URL="{registry_url}" 295REGISTRY_NAMESPACE="${{CONTAINER_REGISTRY_NAMESPACE:-{registry_namespace}}}"
164REGISTRY_NAMESPACE="{registry_namespace}" 296REGISTRY_CONFIG="$REGISTRY_STORAGE/registry-config.yml"
165 297
166# Deploy directories - can be overridden via environment 298# Deploy directories - can be overridden via environment
167# DEPLOY_DIR_IMAGES: parent directory containing per-machine deploy dirs 299# DEPLOY_DIR_IMAGES: parent directory containing per-machine deploy dirs
@@ -173,8 +305,27 @@ DEPLOY_DIR_IMAGE="${{DEPLOY_DIR_IMAGE:-{deploy_dir_image}}}"
173DEFAULT_TAG_STRATEGY="{tag_strategy}" 305DEFAULT_TAG_STRATEGY="{tag_strategy}"
174DEFAULT_TARGET_ARCH="{target_arch}" 306DEFAULT_TARGET_ARCH="{target_arch}"
175 307
176PID_FILE="/tmp/container-registry.pid" 308# Authentication settings (can be overridden via CLI options or env vars)
177LOG_FILE="/tmp/container-registry.log" 309AUTH_MODE="${{CONTAINER_REGISTRY_AUTH_MODE:-none}}"
310AUTHFILE="${{CONTAINER_REGISTRY_AUTHFILE:-}}"
311CREDSFILE="${{CONTAINER_REGISTRY_CREDSFILE:-}}"
312
313# Secure mode settings (baked from bitbake)
314SECURE_MODE="${{CONTAINER_REGISTRY_SECURE:-{secure_mode}}}"
315AUTH_ENABLED="${{CONTAINER_REGISTRY_AUTH:-{auth_enabled}}}"
316REGISTRY_USERNAME="${{CONTAINER_REGISTRY_USERNAME:-{registry_username}}}"
317CA_CERT_DAYS="{ca_days}"
318SERVER_CERT_DAYS="{cert_days}"
319CUSTOM_SAN="{custom_san}"
320
321# Directories for secure mode
322PKI_DIR="$REGISTRY_STORAGE/pki"
323AUTH_DIR="$REGISTRY_STORAGE/auth"
324
325# Port-based PID/LOG files (allows multiple instances on different ports)
326REGISTRY_PORT="${{REGISTRY_URL##*:}}"
327PID_FILE="/tmp/container-registry-$REGISTRY_PORT.pid"
328LOG_FILE="/tmp/container-registry-$REGISTRY_PORT.log"
178 329
179# Generate tags based on strategy 330# Generate tags based on strategy
180# Usage: generate_tags "strategy1 strategy2 ..." 331# Usage: generate_tags "strategy1 strategy2 ..."
@@ -240,7 +391,359 @@ generate_tags() {{
240 echo $tags 391 echo $tags
241}} 392}}
242 393
394# Parse a simple credentials file (key=value format)
395# Sets CONTAINER_REGISTRY_USER, CONTAINER_REGISTRY_PASSWORD, CONTAINER_REGISTRY_TOKEN
396parse_credsfile() {{
397 local file="$1"
398 [ ! -f "$file" ] && {{ echo "Error: Credentials file not found: $file" >&2; return 1; }}
399
400 while IFS='=' read -r key value || [ -n "$key" ]; do
401 # Skip comments and empty lines
402 [[ "$key" =~ ^[[:space:]]*# ]] && continue
403 [[ -z "$key" ]] && continue
404
405 # Trim whitespace
406 key=$(echo "$key" | xargs)
407 value=$(echo "$value" | xargs)
408
409 # Remove surrounding quotes
410 value="${{value#\\"}}"
411 value="${{value%\\"}}"
412 value="${{value#'}}"
413 value="${{value%'}}"
414
415 case "$key" in
416 CONTAINER_REGISTRY_USER) export CONTAINER_REGISTRY_USER="$value" ;;
417 CONTAINER_REGISTRY_PASSWORD) export CONTAINER_REGISTRY_PASSWORD="$value" ;;
418 CONTAINER_REGISTRY_TOKEN) export CONTAINER_REGISTRY_TOKEN="$value" ;;
419 esac
420 done < "$file"
421}}
422
423# ============================================================================
424# Secure Registry PKI and Auth Setup
425# ============================================================================
426
427# Generate PKI infrastructure (CA + server certificate)
428# Creates: $PKI_DIR/ca.key, ca.crt, server.key, server.crt
429setup_pki() {{
430 # Check for openssl
431 if ! command -v openssl >/dev/null 2>&1; then
432 echo "Error: openssl is required for secure mode but not found"
433 echo "Install with: sudo apt install openssl"
434 return 1
435 fi
436
437 mkdir -p "$PKI_DIR"
438
439 # Generate CA if not exists
440 if [ ! -f "$PKI_DIR/ca.key" ] || [ ! -f "$PKI_DIR/ca.crt" ]; then
441 echo "Generating CA certificate..."
442 openssl genrsa -out "$PKI_DIR/ca.key" 4096
443 chmod 600 "$PKI_DIR/ca.key"
444
445 openssl req -new -x509 -days "$CA_CERT_DAYS" \\
446 -key "$PKI_DIR/ca.key" \\
447 -out "$PKI_DIR/ca.crt" \\
448 -subj "/CN=Yocto Container Registry CA/O=Yocto Project"
449
450 echo " Generated CA certificate: $PKI_DIR/ca.crt"
451 else
452 echo " Using existing CA certificate: $PKI_DIR/ca.crt"
453 fi
454
455 # Generate server certificate if not exists
456 if [ ! -f "$PKI_DIR/server.key" ] || [ ! -f "$PKI_DIR/server.crt" ]; then
457 echo "Generating server certificate..."
458
459 # Build SAN list
460 # Extract host from registry URL (strip port)
461 local registry_host=$(echo "$REGISTRY_URL" | cut -d':' -f1)
462 local san_list="DNS:localhost,DNS:$registry_host,IP:127.0.0.1,IP:10.0.2.2"
463
464 # Add custom SAN entries
465 if [ -n "$CUSTOM_SAN" ]; then
466 san_list="$san_list,$CUSTOM_SAN"
467 fi
468
469 echo " SAN entries: $san_list"
470
471 # Create OpenSSL config for SAN
472 local ssl_conf="$PKI_DIR/openssl.cnf"
473 cat > "$ssl_conf" << SSLEOF
474[req]
475distinguished_name = req_distinguished_name
476req_extensions = v3_req
477prompt = no
478
479[req_distinguished_name]
480CN = $registry_host
481O = Yocto Project
482
483[v3_req]
484keyUsage = keyEncipherment, dataEncipherment
485extendedKeyUsage = serverAuth
486subjectAltName = $san_list
487SSLEOF
488
489 # Generate server key
490 openssl genrsa -out "$PKI_DIR/server.key" 2048
491 chmod 600 "$PKI_DIR/server.key"
492
493 # Generate CSR
494 openssl req -new \\
495 -key "$PKI_DIR/server.key" \\
496 -out "$PKI_DIR/server.csr" \\
497 -config "$ssl_conf"
498
499 # Sign with CA
500 openssl x509 -req \\
501 -in "$PKI_DIR/server.csr" \\
502 -CA "$PKI_DIR/ca.crt" \\
503 -CAkey "$PKI_DIR/ca.key" \\
504 -CAcreateserial \\
505 -out "$PKI_DIR/server.crt" \\
506 -days "$SERVER_CERT_DAYS" \\
507 -extensions v3_req \\
508 -extfile "$ssl_conf"
509
510 # Cleanup temp files
511 rm -f "$PKI_DIR/server.csr" "$ssl_conf"
512
513 echo " Generated server certificate with SAN: localhost, $registry_host, 127.0.0.1, 10.0.2.2"
514 else
515 echo " Using existing server certificate: $PKI_DIR/server.crt"
516 fi
517}}
518
519# Setup htpasswd authentication
520# Creates: $AUTH_DIR/htpasswd, $AUTH_DIR/password
521setup_auth() {{
522 # Check for htpasswd (from apache2-utils)
523 if ! command -v htpasswd >/dev/null 2>&1; then
524 echo "Error: htpasswd is required for secure mode but not found"
525 echo "Install with: sudo apt install apache2-utils"
526 return 1
527 fi
528
529 mkdir -p "$AUTH_DIR"
530
531 local password=""
532
533 # Password priority:
534 # 1. CONTAINER_REGISTRY_PASSWORD environment variable
535 # 2. Existing $AUTH_DIR/password file
536 # 3. Auto-generate new password
537 if [ -n "${{CONTAINER_REGISTRY_PASSWORD:-}}" ]; then
538 password="$CONTAINER_REGISTRY_PASSWORD"
539 echo " Using password from environment variable"
540 elif [ -f "$AUTH_DIR/password" ]; then
541 password=$(cat "$AUTH_DIR/password")
542 echo " Using existing password from $AUTH_DIR/password"
543 else
544 # Generate random password (16 chars, alphanumeric)
545 password=$(openssl rand -base64 12 | tr -dc 'a-zA-Z0-9' | head -c 16)
546 echo " Generated new random password"
547 fi
548
549 # Always update htpasswd (in case username changed)
550 echo " Creating htpasswd for user: $REGISTRY_USERNAME"
551 htpasswd -Bbn "$REGISTRY_USERNAME" "$password" > "$AUTH_DIR/htpasswd"
552
553 # Save password for reference (used by script and bbclass)
554 echo -n "$password" > "$AUTH_DIR/password"
555 chmod 600 "$AUTH_DIR/password"
556
557 echo " Password saved to: $AUTH_DIR/password"
558}}
559
560# Get TLS arguments for skopeo
561# Usage: get_tls_args [dest|src]
562# Returns: TLS arguments string for skopeo
563get_tls_args() {{
564 local direction="${{1:-dest}}"
565 local prefix=""
566
567 if [ "$direction" = "src" ]; then
568 prefix="--src"
569 else
570 prefix="--dest"
571 fi
572
573 if [ "$SECURE_MODE" = "1" ] && [ -f "$PKI_DIR/ca.crt" ]; then
574 # skopeo --dest-cert-dir expects a directory with only CA certs
575 # If we point it at PKI_DIR which has ca.key, it thinks it's a client key
576 # Create a clean certs directory with just the CA cert
577 local certs_dir="$REGISTRY_STORAGE/certs"
578 if [ ! -f "$certs_dir/ca.crt" ] || [ "$PKI_DIR/ca.crt" -nt "$certs_dir/ca.crt" ]; then
579 mkdir -p "$certs_dir"
580 cp "$PKI_DIR/ca.crt" "$certs_dir/ca.crt"
581 fi
582 echo "$prefix-cert-dir $certs_dir"
583 else
584 echo "$prefix-tls-verify=false"
585 fi
586}}
587
588# Get the base URL (http or https depending on mode)
589get_base_url() {{
590 if [ "$SECURE_MODE" = "1" ]; then
591 echo "https://$REGISTRY_URL"
592 else
593 echo "http://$REGISTRY_URL"
594 fi
595}}
596
597# Get curl TLS arguments
598get_curl_tls_args() {{
599 if [ "$SECURE_MODE" = "1" ] && [ -f "$PKI_DIR/ca.crt" ]; then
600 echo "--cacert $PKI_DIR/ca.crt"
601 fi
602}}
603
604# Get curl auth arguments (for auth-enabled mode)
605get_curl_auth_args() {{
606 if [ "$AUTH_ENABLED" = "1" ] && [ -f "$AUTH_DIR/password" ]; then
607 local password=$(cat "$AUTH_DIR/password")
608 echo "-u $REGISTRY_USERNAME:$password"
609 fi
610}}
611
612# Build authentication arguments for skopeo based on auth mode
613# Usage: get_auth_args [dest|src]
614# Returns: authentication arguments string for skopeo
615get_auth_args() {{
616 local direction="${{1:-dest}}"
617 local mode="${{AUTH_MODE:-none}}"
618 local prefix=""
619
620 if [ "$direction" = "src" ]; then
621 prefix="--src"
622 else
623 prefix="--dest"
624 fi
625
626 case "$mode" in
627 none)
628 # In auth-enabled mode with no explicit auth, auto-use generated credentials
629 if [ "$AUTH_ENABLED" = "1" ] && [ -f "$AUTH_DIR/password" ]; then
630 local password=$(cat "$AUTH_DIR/password")
631 echo "$prefix-creds $REGISTRY_USERNAME:$password"
632 else
633 echo ""
634 fi
635 ;;
636 home)
637 # Use ~/.docker/config.json (like BB_USE_HOME_NPMRC pattern)
638 local home_auth="$HOME/.docker/config.json"
639 if [ ! -f "$home_auth" ]; then
640 echo "Error: AUTH_MODE=home but $home_auth not found" >&2
641 echo "Run 'docker login' first or use --authfile/--credsfile" >&2
642 return 1
643 fi
644 echo "$prefix-authfile $home_auth"
645 ;;
646 authfile)
647 [ -z "$AUTHFILE" ] && {{ echo "Error: --authfile required" >&2; return 1; }}
648 [ ! -f "$AUTHFILE" ] && {{ echo "Error: Auth file not found: $AUTHFILE" >&2; return 1; }}
649 echo "$prefix-authfile $AUTHFILE"
650 ;;
651 credsfile)
652 [ -z "$CREDSFILE" ] && {{ echo "Error: --credsfile required" >&2; return 1; }}
653 parse_credsfile "$CREDSFILE" || return 1
654 # Fall through to check credentials
655 if [ -n "${{CONTAINER_REGISTRY_TOKEN:-}}" ]; then
656 echo "$prefix-registry-token $CONTAINER_REGISTRY_TOKEN"
657 elif [ -n "${{CONTAINER_REGISTRY_USER:-}}" ] && [ -n "${{CONTAINER_REGISTRY_PASSWORD:-}}" ]; then
658 echo "$prefix-creds $CONTAINER_REGISTRY_USER:$CONTAINER_REGISTRY_PASSWORD"
659 else
660 echo "Error: Credentials file must contain TOKEN or USER+PASSWORD" >&2
661 return 1
662 fi
663 ;;
664 env)
665 # Environment variable mode (script only, not bbclass)
666 if [ -n "${{CONTAINER_REGISTRY_TOKEN:-}}" ]; then
667 echo "$prefix-registry-token $CONTAINER_REGISTRY_TOKEN"
668 elif [ -n "${{CONTAINER_REGISTRY_USER:-}}" ] && [ -n "${{CONTAINER_REGISTRY_PASSWORD:-}}" ]; then
669 echo "$prefix-creds $CONTAINER_REGISTRY_USER:$CONTAINER_REGISTRY_PASSWORD"
670 else
671 echo "Error: AUTH_MODE=env requires CONTAINER_REGISTRY_TOKEN or USER+PASSWORD" >&2
672 return 1
673 fi
674 ;;
675 creds)
676 # Direct credentials (set by --creds option)
677 [ -z "$DIRECT_CREDS" ] && {{ echo "Error: --creds value missing" >&2; return 1; }}
678 echo "$prefix-creds $DIRECT_CREDS"
679 ;;
680 token)
681 # Direct token (set by --token option)
682 [ -z "$DIRECT_TOKEN" ] && {{ echo "Error: --token value missing" >&2; return 1; }}
683 echo "$prefix-registry-token $DIRECT_TOKEN"
684 ;;
685 *)
686 echo "Error: Unknown auth mode: $mode" >&2
687 return 1
688 ;;
689 esac
690}}
691
692# Generate registry config file if it doesn't exist
693# Called automatically on start when REGISTRY_STORAGE is overridden
694generate_config() {{
695 [ -f "$REGISTRY_CONFIG" ] && return 0
696
697 echo "Generating registry config: $REGISTRY_CONFIG"
698 local port="${{REGISTRY_URL##*:}}"
699 mkdir -p "$(dirname "$REGISTRY_CONFIG")"
700
701 {{
702 echo "version: 0.1"
703 echo "log:"
704 echo " level: info"
705 echo " formatter: text"
706 echo "storage:"
707 echo " filesystem:"
708 echo " rootdirectory: $REGISTRY_STORAGE"
709 echo " delete:"
710 echo " enabled: true"
711 echo " redirect:"
712 echo " disable: true"
713 echo "http:"
714 echo " addr: :$port"
715 echo " headers:"
716 echo " X-Content-Type-Options: [nosniff]"
717 if [ "$SECURE_MODE" = "1" ]; then
718 echo " tls:"
719 echo " certificate: $PKI_DIR/server.crt"
720 echo " key: $PKI_DIR/server.key"
721 fi
722 if [ "$AUTH_ENABLED" = "1" ]; then
723 echo "auth:"
724 echo " htpasswd:"
725 echo " realm: Yocto Container Registry"
726 echo " path: $AUTH_DIR/htpasswd"
727 fi
728 echo "health:"
729 echo " storagedriver:"
730 echo " enabled: true"
731 echo " interval: 10s"
732 echo " threshold: 3"
733 }} > "$REGISTRY_CONFIG"
734}}
735
243cmd_start() {{ 736cmd_start() {{
737 # Migration: check old PID file location
738 local old_pid_file="/tmp/container-registry.pid"
739 if [ ! -f "$PID_FILE" ] && [ -f "$old_pid_file" ] && [ "$PID_FILE" != "$old_pid_file" ]; then
740 if kill -0 "$(cat "$old_pid_file")" 2>/dev/null; then
741 PID_FILE="$old_pid_file"
742 else
743 rm -f "$old_pid_file"
744 fi
745 fi
746
244 if [ -f "$PID_FILE" ] && kill -0 "$(cat $PID_FILE)" 2>/dev/null; then 747 if [ -f "$PID_FILE" ] && kill -0 "$(cat $PID_FILE)" 2>/dev/null; then
245 echo "Registry already running (PID: $(cat $PID_FILE))" 748 echo "Registry already running (PID: $(cat $PID_FILE))"
246 return 0 749 return 0
@@ -254,10 +757,37 @@ cmd_start() {{
254 757
255 mkdir -p "$REGISTRY_STORAGE" 758 mkdir -p "$REGISTRY_STORAGE"
256 759
257 echo "Starting container registry..." 760 # Generate config if it doesn't exist (e.g., when using custom REGISTRY_STORAGE)
258 echo " URL: http://$REGISTRY_URL" 761 generate_config
762
763 # Setup PKI for secure mode, auth is optional
764 if [ "$SECURE_MODE" = "1" ]; then
765 echo "Generating PKI infrastructure..."
766 setup_pki || return 1
767 echo ""
768 if [ "$AUTH_ENABLED" = "1" ]; then
769 echo "Setting up authentication..."
770 setup_auth || return 1
771 echo ""
772 echo "Starting SECURE container registry (TLS + auth)..."
773 else
774 echo "Starting SECURE container registry (TLS only)..."
775 fi
776 echo " URL: https://$REGISTRY_URL"
777 else
778 echo "Starting container registry..."
779 echo " URL: http://$REGISTRY_URL"
780 fi
781
259 echo " Storage: $REGISTRY_STORAGE" 782 echo " Storage: $REGISTRY_STORAGE"
260 echo " Config: $REGISTRY_CONFIG" 783 echo " Config: $REGISTRY_CONFIG"
784 if [ "$SECURE_MODE" = "1" ]; then
785 echo " PKI: $PKI_DIR"
786 if [ "$AUTH_ENABLED" = "1" ]; then
787 echo " Auth: $AUTH_DIR"
788 echo " User: $REGISTRY_USERNAME"
789 fi
790 fi
261 791
262 nohup "$REGISTRY_BIN" serve "$REGISTRY_CONFIG" > "$LOG_FILE" 2>&1 & 792 nohup "$REGISTRY_BIN" serve "$REGISTRY_CONFIG" > "$LOG_FILE" 2>&1 &
263 echo $! > "$PID_FILE" 793 echo $! > "$PID_FILE"
@@ -266,6 +796,11 @@ cmd_start() {{
266 if kill -0 "$(cat $PID_FILE)" 2>/dev/null; then 796 if kill -0 "$(cat $PID_FILE)" 2>/dev/null; then
267 echo "Registry started (PID: $(cat $PID_FILE))" 797 echo "Registry started (PID: $(cat $PID_FILE))"
268 echo "Logs: $LOG_FILE" 798 echo "Logs: $LOG_FILE"
799 if [ "$SECURE_MODE" = "1" ]; then
800 echo ""
801 echo "To install CA cert on targets, add to IMAGE_INSTALL:"
802 echo ' IMAGE_INSTALL:append = " container-registry-ca"'
803 fi
269 else 804 else
270 echo "Failed to start registry. Check $LOG_FILE" 805 echo "Failed to start registry. Check $LOG_FILE"
271 cat "$LOG_FILE" 806 cat "$LOG_FILE"
@@ -274,6 +809,12 @@ cmd_start() {{
274}} 809}}
275 810
276cmd_stop() {{ 811cmd_stop() {{
812 # Migration: check old PID file location
813 local old_pid_file="/tmp/container-registry.pid"
814 if [ ! -f "$PID_FILE" ] && [ -f "$old_pid_file" ] && [ "$PID_FILE" != "$old_pid_file" ]; then
815 PID_FILE="$old_pid_file"
816 fi
817
277 if [ ! -f "$PID_FILE" ]; then 818 if [ ! -f "$PID_FILE" ]; then
278 echo "Registry not running" 819 echo "Registry not running"
279 return 0 820 return 0
@@ -292,14 +833,34 @@ cmd_stop() {{
292}} 833}}
293 834
294cmd_status() {{ 835cmd_status() {{
836 # Migration: check old PID file location
837 local old_pid_file="/tmp/container-registry.pid"
838 if [ ! -f "$PID_FILE" ] && [ -f "$old_pid_file" ] && [ "$PID_FILE" != "$old_pid_file" ]; then
839 if kill -0 "$(cat "$old_pid_file")" 2>/dev/null; then
840 PID_FILE="$old_pid_file"
841 else
842 rm -f "$old_pid_file"
843 fi
844 fi
845
295 if [ -f "$PID_FILE" ] && kill -0 "$(cat $PID_FILE)" 2>/dev/null; then 846 if [ -f "$PID_FILE" ] && kill -0 "$(cat $PID_FILE)" 2>/dev/null; then
296 echo "Registry running (PID: $(cat $PID_FILE))" 847 echo "Registry running (PID: $(cat $PID_FILE))"
297 echo "URL: http://$REGISTRY_URL" 848 local base_url=$(get_base_url)
298 if curl -s "http://$REGISTRY_URL/v2/" >/dev/null 2>&1; then 849 echo "URL: $base_url"
850 local tls_args=$(get_curl_tls_args)
851 local auth_args=$(get_curl_auth_args)
852 if curl -s $tls_args $auth_args "$base_url/v2/" >/dev/null 2>&1; then
299 echo "Status: healthy" 853 echo "Status: healthy"
300 else 854 else
301 echo "Status: not responding" 855 echo "Status: not responding"
302 fi 856 fi
857 if [ "$SECURE_MODE" = "1" ]; then
858 if [ "$AUTH_ENABLED" = "1" ]; then
859 echo "Mode: secure (TLS + auth)"
860 else
861 echo "Mode: secure (TLS only)"
862 fi
863 fi
303 else 864 else
304 echo "Registry not running" 865 echo "Registry not running"
305 return 1 866 return 1
@@ -342,9 +903,12 @@ get_oci_arch() {{
342is_manifest_list() {{ 903is_manifest_list() {{
343 local image="$1" 904 local image="$1"
344 local tag="$2" 905 local tag="$2"
906 local base_url=$(get_base_url)
907 local tls_args=$(get_curl_tls_args)
908 local auth_args=$(get_curl_auth_args)
345 909
346 local content_type=$(curl -s -I -H "Accept: application/vnd.oci.image.index.v1+json, application/vnd.docker.distribution.manifest.list.v2+json" \\ 910 local content_type=$(curl -s -I $tls_args $auth_args -H "Accept: application/vnd.oci.image.index.v1+json, application/vnd.docker.distribution.manifest.list.v2+json" \\
347 "http://$REGISTRY_URL/v2/$image/manifests/$tag" 2>/dev/null | grep -i "content-type" | head -1) 911 "$base_url/v2/$image/manifests/$tag" 2>/dev/null | grep -i "content-type" | head -1)
348 912
349 echo "$content_type" | grep -qE "manifest.list|image.index" 913 echo "$content_type" | grep -qE "manifest.list|image.index"
350}} 914}}
@@ -355,9 +919,12 @@ is_manifest_list() {{
355get_manifest_list() {{ 919get_manifest_list() {{
356 local image="$1" 920 local image="$1"
357 local tag="$2" 921 local tag="$2"
922 local base_url=$(get_base_url)
923 local tls_args=$(get_curl_tls_args)
924 local auth_args=$(get_curl_auth_args)
358 925
359 curl -s -H "Accept: application/vnd.oci.image.index.v1+json, application/vnd.docker.distribution.manifest.list.v2+json" \\ 926 curl -s $tls_args $auth_args -H "Accept: application/vnd.oci.image.index.v1+json, application/vnd.docker.distribution.manifest.list.v2+json" \\
360 "http://$REGISTRY_URL/v2/$image/manifests/$tag" 2>/dev/null 927 "$base_url/v2/$image/manifests/$tag" 2>/dev/null
361}} 928}}
362 929
363# Get manifest digest and size for an image by tag 930# Get manifest digest and size for an image by tag
@@ -366,9 +933,12 @@ get_manifest_list() {{
366get_manifest_info() {{ 933get_manifest_info() {{
367 local image="$1" 934 local image="$1"
368 local tag="$2" 935 local tag="$2"
936 local base_url=$(get_base_url)
937 local tls_args=$(get_curl_tls_args)
938 local auth_args=$(get_curl_auth_args)
369 939
370 local headers=$(curl -s -I -H "Accept: application/vnd.oci.image.manifest.v1+json, application/vnd.docker.distribution.manifest.v2+json" \\ 940 local headers=$(curl -s -I $tls_args $auth_args -H "Accept: application/vnd.oci.image.manifest.v1+json, application/vnd.docker.distribution.manifest.v2+json" \\
371 "http://$REGISTRY_URL/v2/$image/manifests/$tag" 2>/dev/null) 941 "$base_url/v2/$image/manifests/$tag" 2>/dev/null)
372 942
373 local digest=$(echo "$headers" | grep -i "docker-content-digest" | awk '{{print $2}}' | tr -d '\\r\\n') 943 local digest=$(echo "$headers" | grep -i "docker-content-digest" | awk '{{print $2}}' | tr -d '\\r\\n')
374 local size=$(echo "$headers" | grep -i "content-length" | awk '{{print $2}}' | tr -d '\\r\\n') 944 local size=$(echo "$headers" | grep -i "content-length" | awk '{{print $2}}' | tr -d '\\r\\n')
@@ -377,24 +947,41 @@ get_manifest_info() {{
377}} 947}}
378 948
379# Push image by digest (returns the digest) 949# Push image by digest (returns the digest)
380# Usage: push_by_digest <oci_dir> <image_name> 950# Usage: push_by_digest <oci_dir> <image_name> [auth_args]
381push_by_digest() {{ 951push_by_digest() {{
382 local oci_dir="$1" 952 local oci_dir="$1"
383 local image_name="$2" 953 local image_name="$2"
954 local push_auth_args="${{3:-}}"
384 local temp_tag="temp-${{RANDOM}}-$(date +%s)" 955 local temp_tag="temp-${{RANDOM}}-$(date +%s)"
385 956
386 # Push with temporary tag 957 # Get TLS arguments
387 "$SKOPEO_BIN" copy --dest-tls-verify=false \\ 958 local tls_args=$(get_tls_args dest)
959
960 # Push with temporary tag (capture output for error debugging)
961 local push_output
962 if ! push_output=$("$SKOPEO_BIN" copy $tls_args $push_auth_args \\
388 "oci:$oci_dir" \\ 963 "oci:$oci_dir" \\
389 "docker://$REGISTRY_URL/$REGISTRY_NAMESPACE/$image_name:$temp_tag" >/dev/null 2>&1 964 "docker://$REGISTRY_URL/$REGISTRY_NAMESPACE/$image_name:$temp_tag" 2>&1); then
965 echo "ERROR: skopeo copy failed: $push_output" >&2
966 return 1
967 fi
390 968
391 # Get digest for the pushed image 969 # Get digest for the pushed image
392 local info=$(get_manifest_info "$REGISTRY_NAMESPACE/$image_name" "$temp_tag") 970 local info=$(get_manifest_info "$REGISTRY_NAMESPACE/$image_name" "$temp_tag")
393 local digest=$(echo "$info" | cut -d: -f1-2) # sha256:xxx 971 local digest=$(echo "$info" | cut -d: -f1-2) # sha256:xxx
394 local size=$(echo "$info" | cut -d: -f3) 972 local size=$(echo "$info" | cut -d: -f3)
395 973
974 # Validate we got both digest and size
975 if [ -z "$digest" ] || [ -z "$size" ]; then
976 echo "ERROR: Failed to get manifest info for pushed image (digest=$digest, size=$size)" >&2
977 return 1
978 fi
979
396 # Delete the temp tag (leave the blobs) 980 # Delete the temp tag (leave the blobs)
397 curl -s -X DELETE "http://$REGISTRY_URL/v2/$REGISTRY_NAMESPACE/$image_name/manifests/$temp_tag" >/dev/null 2>&1 || true 981 local base_url=$(get_base_url)
982 local curl_tls_args=$(get_curl_tls_args)
983 local curl_auth_args=$(get_curl_auth_args)
984 curl -s -X DELETE $curl_tls_args $curl_auth_args "$base_url/v2/$REGISTRY_NAMESPACE/$image_name/manifests/$temp_tag" >/dev/null 2>&1 || true
398 985
399 echo "$digest:$size" 986 echo "$digest:$size"
400}} 987}}
@@ -440,12 +1027,13 @@ except: pass
440 local existing_size=$(echo "$existing_info" | cut -d: -f3) 1027 local existing_size=$(echo "$existing_info" | cut -d: -f3)
441 1028
442 # Inspect to get architecture 1029 # Inspect to get architecture
443 local existing_arch=$("$SKOPEO_BIN" inspect --tls-verify=false \\ 1030 local inspect_tls_args=$(get_tls_args dest | sed 's/--dest/--/')
1031 local existing_arch=$("$SKOPEO_BIN" inspect $inspect_tls_args \\
444 "docker://$REGISTRY_URL/$image:$tag" 2>/dev/null | \\ 1032 "docker://$REGISTRY_URL/$image:$tag" 2>/dev/null | \\
445 python3 -c "import sys,json; print(json.load(sys.stdin).get('Architecture',''))" 2>/dev/null) 1033 python3 -c "import sys,json; print(json.load(sys.stdin).get('Architecture',''))" 2>/dev/null)
446 1034
447 if [ -n "$existing_arch" ] && [ "$existing_arch" != "$new_arch" ]; then 1035 if [ -n "$existing_arch" ] && [ "$existing_arch" != "$new_arch" ] && [ -n "$existing_size" ]; then
448 # Different arch - include it in manifest list 1036 # Different arch - include it in manifest list (only if we have valid size)
449 manifests=$(cat <<MANIFEST 1037 manifests=$(cat <<MANIFEST
450{{"mediaType": "application/vnd.oci.image.manifest.v1+json", "digest": "$existing_digest", "size": $existing_size, "platform": {{"architecture": "$existing_arch", "os": "linux"}}}} 1038{{"mediaType": "application/vnd.oci.image.manifest.v1+json", "digest": "$existing_digest", "size": $existing_size, "platform": {{"architecture": "$existing_arch", "os": "linux"}}}}
451MANIFEST 1039MANIFEST
@@ -454,6 +1042,12 @@ MANIFEST
454 fi 1042 fi
455 fi 1043 fi
456 1044
1045 # Validate required parameters
1046 if [ -z "$new_digest" ] || [ -z "$new_size" ] || [ -z "$new_arch" ]; then
1047 echo "ERROR: Missing required manifest parameters (digest=$new_digest, size=$new_size, arch=$new_arch)" >&2
1048 return 1
1049 fi
1050
457 # Add our new manifest 1051 # Add our new manifest
458 local new_manifest='{{"mediaType": "application/vnd.oci.image.manifest.v1+json", "digest": "'$new_digest'", "size": '$new_size', "platform": {{"architecture": "'$new_arch'", "os": "'$new_os'"}}}}' 1052 local new_manifest='{{"mediaType": "application/vnd.oci.image.manifest.v1+json", "digest": "'$new_digest'", "size": '$new_size', "platform": {{"architecture": "'$new_arch'", "os": "'$new_os'"}}}}'
459 1053
@@ -465,13 +1059,22 @@ $new_manifest"
465 fi 1059 fi
466 1060
467 # Create manifest list JSON 1061 # Create manifest list JSON
468 local manifest_list=$(python3 -c " 1062 local manifest_list
1063 manifest_list=$(python3 -c "
469import sys, json 1064import sys, json
470manifests = [] 1065manifests = []
471for line in sys.stdin: 1066for i, line in enumerate(sys.stdin):
472 line = line.strip() 1067 line = line.strip()
473 if line: 1068 if line:
474 manifests.append(json.loads(line)) 1069 try:
1070 manifests.append(json.loads(line))
1071 except json.JSONDecodeError as e:
1072 print(f'ERROR: Invalid JSON on line {{i+1}}: {{e}}', file=sys.stderr)
1073 print(f' Content: {{line[:100]}}...', file=sys.stderr)
1074 sys.exit(1)
1075if not manifests:
1076 print('ERROR: No valid manifests to create list', file=sys.stderr)
1077 sys.exit(1)
475result = {{ 1078result = {{
476 'schemaVersion': 2, 1079 'schemaVersion': 2,
477 'mediaType': 'application/vnd.oci.image.index.v1+json', 1080 'mediaType': 'application/vnd.oci.image.index.v1+json',
@@ -480,11 +1083,20 @@ result = {{
480print(json.dumps(result, indent=2)) 1083print(json.dumps(result, indent=2))
481" <<< "$manifests") 1084" <<< "$manifests")
482 1085
1086 if [ -z "$manifest_list" ]; then
1087 echo "ERROR: Failed to create manifest list" >&2
1088 return 1
1089 fi
1090
483 # Push manifest list 1091 # Push manifest list
1092 local base_url=$(get_base_url)
1093 local curl_tls_args=$(get_curl_tls_args)
1094 local curl_auth_args=$(get_curl_auth_args)
484 local status=$(curl -s -o /dev/null -w "%{{http_code}}" -X PUT \\ 1095 local status=$(curl -s -o /dev/null -w "%{{http_code}}" -X PUT \\
1096 $curl_tls_args $curl_auth_args \\
485 -H "Content-Type: application/vnd.oci.image.index.v1+json" \\ 1097 -H "Content-Type: application/vnd.oci.image.index.v1+json" \\
486 -d "$manifest_list" \\ 1098 -d "$manifest_list" \\
487 "http://$REGISTRY_URL/v2/$image/manifests/$tag") 1099 "$base_url/v2/$image/manifests/$tag")
488 1100
489 [ "$status" = "201" ] || [ "$status" = "200" ] 1101 [ "$status" = "201" ] || [ "$status" = "200" ]
490}} 1102}}
@@ -512,6 +1124,35 @@ cmd_push() {{
512 version="$2" 1124 version="$2"
513 shift 2 1125 shift 2
514 ;; 1126 ;;
1127 # Authentication options
1128 --auth-mode)
1129 AUTH_MODE="$2"
1130 shift 2
1131 ;;
1132 --use-home-auth)
1133 AUTH_MODE="home"
1134 shift
1135 ;;
1136 --authfile)
1137 AUTH_MODE="authfile"
1138 AUTHFILE="$2"
1139 shift 2
1140 ;;
1141 --credsfile)
1142 AUTH_MODE="credsfile"
1143 CREDSFILE="$2"
1144 shift 2
1145 ;;
1146 --creds)
1147 AUTH_MODE="creds"
1148 DIRECT_CREDS="$2"
1149 shift 2
1150 ;;
1151 --token)
1152 AUTH_MODE="token"
1153 DIRECT_TOKEN="$2"
1154 shift 2
1155 ;;
515 -*) 1156 -*)
516 echo "Unknown option: $1" 1157 echo "Unknown option: $1"
517 return 1 1158 return 1
@@ -543,12 +1184,19 @@ cmd_push() {{
543 # Export version for generate_tags 1184 # Export version for generate_tags
544 export IMAGE_VERSION="$version" 1185 export IMAGE_VERSION="$version"
545 1186
546 if ! curl -s "http://$REGISTRY_URL/v2/" >/dev/null 2>&1; then 1187 local base_url=$(get_base_url)
547 echo "Registry not responding at http://$REGISTRY_URL" 1188 local curl_tls_args=$(get_curl_tls_args)
1189 local curl_auth_args=$(get_curl_auth_args)
1190 if ! curl -s $curl_tls_args $curl_auth_args "$base_url/v2/" >/dev/null 2>&1; then
1191 echo "Registry not responding at $base_url"
548 echo "Start it first: $0 start" 1192 echo "Start it first: $0 start"
549 return 1 1193 return 1
550 fi 1194 fi
551 1195
1196 # Get authentication arguments
1197 local auth_args
1198 auth_args=$(get_auth_args dest) || return 1
1199
552 # Determine tags to use 1200 # Determine tags to use
553 local tags 1201 local tags
554 if [ -n "$explicit_tags" ]; then 1202 if [ -n "$explicit_tags" ]; then
@@ -575,12 +1223,16 @@ cmd_push() {{
575 echo "" 1223 echo ""
576 1224
577 echo " Uploading image blobs..." 1225 echo " Uploading image blobs..."
578 local digest_info=$(push_by_digest "$oci_dir" "$name") 1226 local digest_info
1227 if ! digest_info=$(push_by_digest "$oci_dir" "$name" "$auth_args"); then
1228 echo " ERROR: Failed to push image"
1229 return 1
1230 fi
579 local digest=$(echo "$digest_info" | cut -d: -f1-2) 1231 local digest=$(echo "$digest_info" | cut -d: -f1-2)
580 local size=$(echo "$digest_info" | cut -d: -f3) 1232 local size=$(echo "$digest_info" | cut -d: -f3)
581 1233
582 if [ -z "$digest" ]; then 1234 if [ -z "$digest" ] || [ -z "$size" ]; then
583 echo " ERROR: Failed to push image" 1235 echo " ERROR: Failed to get image digest/size (digest=$digest, size=$size)"
584 return 1 1236 return 1
585 fi 1237 fi
586 1238
@@ -592,7 +1244,7 @@ cmd_push() {{
592 echo " -> $REGISTRY_URL/$REGISTRY_NAMESPACE/$name:$tag (manifest list)" 1244 echo " -> $REGISTRY_URL/$REGISTRY_NAMESPACE/$name:$tag (manifest list)"
593 else 1245 else
594 echo " WARNING: Failed to update manifest list, falling back to direct push" 1246 echo " WARNING: Failed to update manifest list, falling back to direct push"
595 "$SKOPEO_BIN" copy --dest-tls-verify=false \\ 1247 "$SKOPEO_BIN" copy --dest-tls-verify=false $auth_args \\
596 "oci:$oci_dir" \\ 1248 "oci:$oci_dir" \\
597 "docker://$REGISTRY_URL/$REGISTRY_NAMESPACE/$name:$tag" 1249 "docker://$REGISTRY_URL/$REGISTRY_NAMESPACE/$name:$tag"
598 fi 1250 fi
@@ -656,12 +1308,16 @@ cmd_push() {{
656 1308
657 # Push image by digest first 1309 # Push image by digest first
658 echo " Uploading image blobs..." 1310 echo " Uploading image blobs..."
659 local digest_info=$(push_by_digest "$oci_dir" "$name") 1311 local digest_info
1312 if ! digest_info=$(push_by_digest "$oci_dir" "$name" "$auth_args"); then
1313 echo " ERROR: Failed to push image"
1314 continue
1315 fi
660 local digest=$(echo "$digest_info" | cut -d: -f1-2) 1316 local digest=$(echo "$digest_info" | cut -d: -f1-2)
661 local size=$(echo "$digest_info" | cut -d: -f3) 1317 local size=$(echo "$digest_info" | cut -d: -f3)
662 1318
663 if [ -z "$digest" ]; then 1319 if [ -z "$digest" ] || [ -z "$size" ]; then
664 echo " ERROR: Failed to push image" 1320 echo " ERROR: Failed to get image digest/size (digest=$digest, size=$size)"
665 continue 1321 continue
666 fi 1322 fi
667 1323
@@ -674,7 +1330,7 @@ cmd_push() {{
674 echo " -> $REGISTRY_URL/$REGISTRY_NAMESPACE/$name:$tag (manifest list)" 1330 echo " -> $REGISTRY_URL/$REGISTRY_NAMESPACE/$name:$tag (manifest list)"
675 else 1331 else
676 echo " WARNING: Failed to update manifest list, falling back to direct push" 1332 echo " WARNING: Failed to update manifest list, falling back to direct push"
677 "$SKOPEO_BIN" copy --dest-tls-verify=false \\ 1333 "$SKOPEO_BIN" copy --dest-tls-verify=false $auth_args \\
678 "oci:$oci_dir" \\ 1334 "oci:$oci_dir" \\
679 "docker://$REGISTRY_URL/$REGISTRY_NAMESPACE/$name:$tag" 1335 "docker://$REGISTRY_URL/$REGISTRY_NAMESPACE/$name:$tag"
680 fi 1336 fi
@@ -706,8 +1362,11 @@ cmd_push() {{
706}} 1362}}
707 1363
708cmd_catalog() {{ 1364cmd_catalog() {{
709 curl -s "http://$REGISTRY_URL/v2/_catalog" | python3 -m json.tool 2>/dev/null || \\ 1365 local base_url=$(get_base_url)
710 curl -s "http://$REGISTRY_URL/v2/_catalog" 1366 local tls_args=$(get_curl_tls_args)
1367 local auth_args=$(get_curl_auth_args)
1368 curl -s $tls_args $auth_args "$base_url/v2/_catalog" | python3 -m json.tool 2>/dev/null || \\
1369 curl -s $tls_args $auth_args "$base_url/v2/_catalog"
711}} 1370}}
712 1371
713cmd_tags() {{ 1372cmd_tags() {{
@@ -727,7 +1386,10 @@ cmd_tags() {{
727 image="$REGISTRY_NAMESPACE/$image" 1386 image="$REGISTRY_NAMESPACE/$image"
728 fi 1387 fi
729 1388
730 local result=$(curl -s "http://$REGISTRY_URL/v2/$image/tags/list") 1389 local base_url=$(get_base_url)
1390 local tls_args=$(get_curl_tls_args)
1391 local auth_args=$(get_curl_auth_args)
1392 local result=$(curl -s $tls_args $auth_args "$base_url/v2/$image/tags/list")
731 1393
732 # Check for errors or empty result 1394 # Check for errors or empty result
733 if [ -z "$result" ]; then 1395 if [ -z "$result" ]; then
@@ -750,15 +1412,19 @@ cmd_tags() {{
750}} 1412}}
751 1413
752cmd_list() {{ 1414cmd_list() {{
753 if ! curl -s "http://$REGISTRY_URL/v2/" >/dev/null 2>&1; then 1415 local base_url=$(get_base_url)
754 echo "Registry not responding at http://$REGISTRY_URL" 1416 local tls_args=$(get_curl_tls_args)
1417 local auth_args=$(get_curl_auth_args)
1418
1419 if ! curl -s $tls_args $auth_args "$base_url/v2/" >/dev/null 2>&1; then
1420 echo "Registry not responding at $base_url"
755 return 1 1421 return 1
756 fi 1422 fi
757 1423
758 echo "Images in $REGISTRY_URL:" 1424 echo "Images in $REGISTRY_URL:"
759 echo "" 1425 echo ""
760 1426
761 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) 1427 local repos=$(curl -s $tls_args $auth_args "$base_url/v2/_catalog" | python3 -c "import sys,json; print('\\n'.join(json.load(sys.stdin).get('repositories',[])))" 2>/dev/null)
762 1428
763 if [ -z "$repos" ]; then 1429 if [ -z "$repos" ]; then
764 echo " (none)" 1430 echo " (none)"
@@ -766,7 +1432,7 @@ cmd_list() {{
766 fi 1432 fi
767 1433
768 for repo in $repos; do 1434 for repo in $repos; do
769 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) 1435 local tags=$(curl -s $tls_args $auth_args "$base_url/v2/$repo/tags/list" | python3 -c "import sys,json; print(' '.join(json.load(sys.stdin).get('tags',[])))" 2>/dev/null)
770 if [ -n "$tags" ]; then 1436 if [ -n "$tags" ]; then
771 echo " $repo: $tags" 1437 echo " $repo: $tags"
772 else 1438 else
@@ -776,22 +1442,78 @@ cmd_list() {{
776}} 1442}}
777 1443
778cmd_import() {{ 1444cmd_import() {{
779 local source="${{2:-}}" 1445 shift # Remove 'import' from args
780 local dest_name="${{3:-}}" 1446
1447 local source=""
1448 local dest_name=""
1449 local src_auth_args=""
1450
1451 # Parse options
1452 while [ $# -gt 0 ]; do
1453 case "$1" in
1454 # Source registry authentication options
1455 --src-authfile)
1456 src_auth_args="--src-authfile $2"
1457 shift 2
1458 ;;
1459 --src-credsfile)
1460 parse_credsfile "$2" || return 1
1461 if [ -n "${{CONTAINER_REGISTRY_TOKEN:-}}" ]; then
1462 src_auth_args="--src-registry-token $CONTAINER_REGISTRY_TOKEN"
1463 elif [ -n "${{CONTAINER_REGISTRY_USER:-}}" ] && [ -n "${{CONTAINER_REGISTRY_PASSWORD:-}}" ]; then
1464 src_auth_args="--src-creds $CONTAINER_REGISTRY_USER:$CONTAINER_REGISTRY_PASSWORD"
1465 else
1466 echo "Error: Credentials file must contain TOKEN or USER+PASSWORD" >&2
1467 return 1
1468 fi
1469 shift 2
1470 ;;
1471 --src-creds)
1472 src_auth_args="--src-creds $2"
1473 shift 2
1474 ;;
1475 --src-token)
1476 src_auth_args="--src-registry-token $2"
1477 shift 2
1478 ;;
1479 -*)
1480 echo "Unknown option: $1"
1481 return 1
1482 ;;
1483 *)
1484 # Positional args: source, then dest_name
1485 if [ -z "$source" ]; then
1486 source="$1"
1487 elif [ -z "$dest_name" ]; then
1488 dest_name="$1"
1489 fi
1490 shift
1491 ;;
1492 esac
1493 done
781 1494
782 if [ -z "$source" ]; then 1495 if [ -z "$source" ]; then
783 echo "Usage: $0 import <source-image> [local-name]" 1496 echo "Usage: $0 import <source-image> [local-name] [options]"
784 echo "" 1497 echo ""
785 echo "Examples:" 1498 echo "Examples:"
786 echo " $0 import docker.io/library/alpine:latest" 1499 echo " $0 import docker.io/library/alpine:latest"
787 echo " $0 import docker.io/library/alpine:latest my-alpine" 1500 echo " $0 import docker.io/library/alpine:latest my-alpine"
788 echo " $0 import quay.io/podman/hello:latest hello" 1501 echo " $0 import quay.io/podman/hello:latest hello"
789 echo " $0 import ghcr.io/owner/image:tag" 1502 echo " $0 import ghcr.io/owner/image:tag"
1503 echo ""
1504 echo "Authentication options (for source registry):"
1505 echo " --src-authfile <path> Docker config.json for source"
1506 echo " --src-credsfile <path> Credentials file for source"
1507 echo " --src-creds <user:pass> Direct credentials for source"
1508 echo " --src-token <token> Bearer token for source"
790 return 1 1509 return 1
791 fi 1510 fi
792 1511
793 if ! curl -s "http://$REGISTRY_URL/v2/" >/dev/null 2>&1; then 1512 local base_url=$(get_base_url)
794 echo "Registry not responding at http://$REGISTRY_URL" 1513 local curl_tls_args=$(get_curl_tls_args)
1514 local curl_auth_args=$(get_curl_auth_args)
1515 if ! curl -s $curl_tls_args $curl_auth_args "$base_url/v2/" >/dev/null 2>&1; then
1516 echo "Registry not responding at $base_url"
795 echo "Start it first: $0 start" 1517 echo "Start it first: $0 start"
796 return 1 1518 return 1
797 fi 1519 fi
@@ -813,8 +1535,14 @@ cmd_import() {{
813 echo " To: $REGISTRY_URL/$REGISTRY_NAMESPACE/$dest_name:$tag" 1535 echo " To: $REGISTRY_URL/$REGISTRY_NAMESPACE/$dest_name:$tag"
814 echo "" 1536 echo ""
815 1537
1538 # Get destination TLS and auth arguments
1539 local dest_tls_args=$(get_tls_args dest)
1540 local dest_auth_args=$(get_auth_args dest) || return 1
1541
816 "$SKOPEO_BIN" copy \\ 1542 "$SKOPEO_BIN" copy \\
817 --dest-tls-verify=false \\ 1543 $dest_tls_args \\
1544 $dest_auth_args \\
1545 $src_auth_args \\
818 "docker://$source" \\ 1546 "docker://$source" \\
819 "docker://$REGISTRY_URL/$REGISTRY_NAMESPACE/$dest_name:$tag" 1547 "docker://$REGISTRY_URL/$REGISTRY_NAMESPACE/$dest_name:$tag"
820 1548
@@ -841,8 +1569,12 @@ cmd_delete() {{
841 return 1 1569 return 1
842 fi 1570 fi
843 1571
844 if ! curl -s "http://$REGISTRY_URL/v2/" >/dev/null 2>&1; then 1572 local base_url=$(get_base_url)
845 echo "Registry not responding at http://$REGISTRY_URL" 1573 local tls_args=$(get_curl_tls_args)
1574 local auth_args=$(get_curl_auth_args)
1575
1576 if ! curl -s $tls_args $auth_args "$base_url/v2/" >/dev/null 2>&1; then
1577 echo "Registry not responding at $base_url"
846 return 1 1578 return 1
847 fi 1579 fi
848 1580
@@ -868,8 +1600,8 @@ cmd_delete() {{
868 local digest="" 1600 local digest=""
869 for accept in "application/vnd.oci.image.manifest.v1+json" \ 1601 for accept in "application/vnd.oci.image.manifest.v1+json" \
870 "application/vnd.docker.distribution.manifest.v2+json"; do 1602 "application/vnd.docker.distribution.manifest.v2+json"; do
871 digest=$(curl -s -I -H "Accept: $accept" \ 1603 digest=$(curl -s -I $tls_args $auth_args -H "Accept: $accept" \
872 "http://$REGISTRY_URL/v2/$name/manifests/$tag" 2>/dev/null \ 1604 "$base_url/v2/$name/manifests/$tag" 2>/dev/null \
873 | grep -i "docker-content-digest" | awk '{{print $2}}' | tr -d '\r\n') 1605 | grep -i "docker-content-digest" | awk '{{print $2}}' | tr -d '\r\n')
874 [ -n "$digest" ] && break 1606 [ -n "$digest" ] && break
875 done 1607 done
@@ -883,7 +1615,8 @@ cmd_delete() {{
883 1615
884 # Delete by digest 1616 # Delete by digest
885 local status=$(curl -s -o /dev/null -w "%{{http_code}}" -X DELETE \ 1617 local status=$(curl -s -o /dev/null -w "%{{http_code}}" -X DELETE \
886 "http://$REGISTRY_URL/v2/$name/manifests/$digest") 1618 $tls_args $auth_args \
1619 "$base_url/v2/$name/manifests/$digest")
887 1620
888 if [ "$status" = "202" ]; then 1621 if [ "$status" = "202" ]; then
889 echo " Deleted successfully" 1622 echo " Deleted successfully"
@@ -965,6 +1698,20 @@ cmd_help() {{
965 echo " --strategy, -s <str> Tag strategy (default: $DEFAULT_TAG_STRATEGY)" 1698 echo " --strategy, -s <str> Tag strategy (default: $DEFAULT_TAG_STRATEGY)"
966 echo " --version, -v <ver> Version for semver strategy (e.g., 1.2.3)" 1699 echo " --version, -v <ver> Version for semver strategy (e.g., 1.2.3)"
967 echo "" 1700 echo ""
1701 echo "Authentication options (for push command):"
1702 echo " --use-home-auth Use ~/.docker/config.json (like BB_USE_HOME_NPMRC)"
1703 echo " --authfile <path> Docker-style config.json file"
1704 echo " --credsfile <path> Simple key=value credentials file"
1705 echo " --creds <user:pass> Direct credentials (less secure)"
1706 echo " --token <token> Bearer token directly (less secure)"
1707 echo " --auth-mode <mode> Mode: none, home, authfile, credsfile, env"
1708 echo ""
1709 echo "Import authentication options (for source registry):"
1710 echo " --src-authfile <path> Docker config.json for source"
1711 echo " --src-credsfile <path> Credentials file for source"
1712 echo " --src-creds <user:pass> Direct credentials for source"
1713 echo " --src-token <token> Bearer token for source"
1714 echo ""
968 echo "Tag strategies (can combine: 'sha branch latest'):" 1715 echo "Tag strategies (can combine: 'sha branch latest'):"
969 echo " timestamp YYYYMMDD-HHMMSS format" 1716 echo " timestamp YYYYMMDD-HHMMSS format"
970 echo " sha, git Short git commit hash" 1717 echo " sha, git Short git commit hash"
@@ -994,8 +1741,19 @@ cmd_help() {{
994 echo " $0 push container-base -t latest -t v1.0.0 # Multiple explicit tags" 1741 echo " $0 push container-base -t latest -t v1.0.0 # Multiple explicit tags"
995 echo " $0 push --strategy 'sha branch latest' # All images, strategy" 1742 echo " $0 push --strategy 'sha branch latest' # All images, strategy"
996 echo " $0 push --strategy semver --version 1.2.3 # All images, SemVer" 1743 echo " $0 push --strategy semver --version 1.2.3 # All images, SemVer"
1744 echo ""
1745 echo "Authentication examples:"
1746 echo " $0 push --use-home-auth # Use ~/.docker/config.json"
1747 echo " $0 push --authfile /path/to/auth.json # Explicit auth file"
1748 echo " $0 push --credsfile ~/.config/creds # Simple credentials file"
1749 echo " $0 import ghcr.io/org/img:v1 --src-credsfile ~/.config/ghcr-creds"
1750 echo ""
1751 echo "Import examples:"
997 echo " $0 import docker.io/library/alpine:latest" 1752 echo " $0 import docker.io/library/alpine:latest"
998 echo " $0 import docker.io/library/busybox:latest my-busybox" 1753 echo " $0 import docker.io/library/busybox:latest my-busybox"
1754 echo " $0 import ghcr.io/org/private:v1 --src-authfile ~/.docker/config.json"
1755 echo ""
1756 echo "Other examples:"
999 echo " $0 delete container-base:20260112-143022" 1757 echo " $0 delete container-base:20260112-143022"
1000 echo " $0 list" 1758 echo " $0 list"
1001 echo " $0 tags container-base" 1759 echo " $0 tags container-base"
@@ -1007,6 +1765,14 @@ cmd_help() {{
1007 echo " IMAGE_VERSION Version for semver/version strategies" 1765 echo " IMAGE_VERSION Version for semver/version strategies"
1008 echo " TARGET_ARCH Architecture for arch strategy" 1766 echo " TARGET_ARCH Architecture for arch strategy"
1009 echo "" 1767 echo ""
1768 echo "Authentication environment variables:"
1769 echo " CONTAINER_REGISTRY_AUTH_MODE Auth mode: none, home, authfile, credsfile, env"
1770 echo " CONTAINER_REGISTRY_AUTHFILE Path to Docker config.json"
1771 echo " CONTAINER_REGISTRY_CREDSFILE Path to simple credentials file"
1772 echo " CONTAINER_REGISTRY_USER Username (env mode only)"
1773 echo " CONTAINER_REGISTRY_PASSWORD Password (env mode only)"
1774 echo " CONTAINER_REGISTRY_TOKEN Token (env mode only)"
1775 echo ""
1010 echo "Configuration (baked from bitbake):" 1776 echo "Configuration (baked from bitbake):"
1011 echo " Registry URL: $REGISTRY_URL" 1777 echo " Registry URL: $REGISTRY_URL"
1012 echo " Namespace: $REGISTRY_NAMESPACE" 1778 echo " Namespace: $REGISTRY_NAMESPACE"
@@ -1014,6 +1780,26 @@ cmd_help() {{
1014 echo " Target arch: $DEFAULT_TARGET_ARCH" 1780 echo " Target arch: $DEFAULT_TARGET_ARCH"
1015 echo " Storage: $REGISTRY_STORAGE" 1781 echo " Storage: $REGISTRY_STORAGE"
1016 echo " Deploy dirs: $DEPLOY_DIR_IMAGES/*/" 1782 echo " Deploy dirs: $DEPLOY_DIR_IMAGES/*/"
1783 if [ "$SECURE_MODE" = "1" ]; then
1784 echo ""
1785 if [ "$AUTH_ENABLED" = "1" ]; then
1786 echo "Secure mode: ENABLED (TLS + authentication)"
1787 else
1788 echo "Secure mode: ENABLED (TLS only)"
1789 fi
1790 echo " PKI directory: $PKI_DIR"
1791 echo ""
1792 echo " CA certificate: $PKI_DIR/ca.crt"
1793 echo ' Install on targets: IMAGE_INSTALL:append = " container-registry-ca"'
1794 if [ "$AUTH_ENABLED" = "1" ]; then
1795 echo ""
1796 echo "Authentication: ENABLED"
1797 echo " Auth directory: $AUTH_DIR"
1798 echo " Username: $REGISTRY_USERNAME"
1799 echo " Password file: $AUTH_DIR/password"
1800 echo " View password: cat $AUTH_DIR/password"
1801 fi
1802 fi
1017}} 1803}}
1018 1804
1019case "${{1:-help}}" in 1805case "${{1:-help}}" in
@@ -1052,7 +1838,7 @@ esac
1052 bb.plain("") 1838 bb.plain("")
1053} 1839}
1054 1840
1055do_generate_registry_script[depends] += "docker-distribution-native:do_populate_sysroot skopeo-native:do_populate_sysroot" 1841do_generate_registry_script[depends] += "docker-distribution-native:do_populate_sysroot skopeo-native:do_populate_sysroot openssl-native:do_populate_sysroot"
1056addtask do_generate_registry_script 1842addtask do_generate_registry_script
1057 1843
1058EXCLUDE_FROM_WORLD = "1" 1844EXCLUDE_FROM_WORLD = "1"