diff options
| author | Bruce Ashfield <bruce.ashfield@gmail.com> | 2026-02-09 03:17:13 +0000 |
|---|---|---|
| committer | Bruce Ashfield <bruce.ashfield@gmail.com> | 2026-02-09 03:34:12 +0000 |
| commit | 092aa81983335b2346a725eebd2a75fc785bb42b (patch) | |
| tree | 4cae1a055b5027c9a049004a339bc3f752cbf8f1 /recipes-containers/container-registry/container-registry-index.bb | |
| parent | 4ed680e32e3670a4e50038387572ee7a35374c0e (diff) | |
| download | meta-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.bb | 904 |
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] | ||
| 193 | distinguished_name = req_distinguished_name | ||
| 194 | req_extensions = v3_req | ||
| 195 | prompt = no | ||
| 196 | |||
| 197 | [req_distinguished_name] | ||
| 198 | CN = {registry_host} | ||
| 199 | O = Yocto | ||
| 200 | C = US | ||
| 201 | |||
| 202 | [v3_req] | ||
| 203 | basicConstraints = CA:FALSE | ||
| 204 | keyUsage = nonRepudiation, digitalSignature, keyEncipherment | ||
| 205 | subjectAltName = {san_string} | ||
| 206 | |||
| 207 | [v3_ca] | ||
| 208 | subjectAltName = {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 | ||
| 156 | set -e | 288 | set -e |
| 157 | 289 | ||
| 158 | # Pre-configured paths from bitbake | 290 | # Pre-configured paths from bitbake (overridable via environment) |
| 159 | REGISTRY_BIN="{registry_bin}" | 291 | REGISTRY_BIN="{registry_bin}" |
| 160 | SKOPEO_BIN="{skopeo_bin}" | 292 | SKOPEO_BIN="{skopeo_bin}" |
| 161 | REGISTRY_CONFIG="{config_file}" | 293 | REGISTRY_STORAGE="${{CONTAINER_REGISTRY_STORAGE:-{registry_storage}}}" |
| 162 | REGISTRY_STORAGE="{registry_storage}" | 294 | REGISTRY_URL="${{CONTAINER_REGISTRY_URL:-{registry_url}}}" |
| 163 | REGISTRY_URL="{registry_url}" | 295 | REGISTRY_NAMESPACE="${{CONTAINER_REGISTRY_NAMESPACE:-{registry_namespace}}}" |
| 164 | REGISTRY_NAMESPACE="{registry_namespace}" | 296 | REGISTRY_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}}}" | |||
| 173 | DEFAULT_TAG_STRATEGY="{tag_strategy}" | 305 | DEFAULT_TAG_STRATEGY="{tag_strategy}" |
| 174 | DEFAULT_TARGET_ARCH="{target_arch}" | 306 | DEFAULT_TARGET_ARCH="{target_arch}" |
| 175 | 307 | ||
| 176 | PID_FILE="/tmp/container-registry.pid" | 308 | # Authentication settings (can be overridden via CLI options or env vars) |
| 177 | LOG_FILE="/tmp/container-registry.log" | 309 | AUTH_MODE="${{CONTAINER_REGISTRY_AUTH_MODE:-none}}" |
| 310 | AUTHFILE="${{CONTAINER_REGISTRY_AUTHFILE:-}}" | ||
| 311 | CREDSFILE="${{CONTAINER_REGISTRY_CREDSFILE:-}}" | ||
| 312 | |||
| 313 | # Secure mode settings (baked from bitbake) | ||
| 314 | SECURE_MODE="${{CONTAINER_REGISTRY_SECURE:-{secure_mode}}}" | ||
| 315 | AUTH_ENABLED="${{CONTAINER_REGISTRY_AUTH:-{auth_enabled}}}" | ||
| 316 | REGISTRY_USERNAME="${{CONTAINER_REGISTRY_USERNAME:-{registry_username}}}" | ||
| 317 | CA_CERT_DAYS="{ca_days}" | ||
| 318 | SERVER_CERT_DAYS="{cert_days}" | ||
| 319 | CUSTOM_SAN="{custom_san}" | ||
| 320 | |||
| 321 | # Directories for secure mode | ||
| 322 | PKI_DIR="$REGISTRY_STORAGE/pki" | ||
| 323 | AUTH_DIR="$REGISTRY_STORAGE/auth" | ||
| 324 | |||
| 325 | # Port-based PID/LOG files (allows multiple instances on different ports) | ||
| 326 | REGISTRY_PORT="${{REGISTRY_URL##*:}}" | ||
| 327 | PID_FILE="/tmp/container-registry-$REGISTRY_PORT.pid" | ||
| 328 | LOG_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 | ||
| 396 | parse_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 | ||
| 429 | setup_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] | ||
| 475 | distinguished_name = req_distinguished_name | ||
| 476 | req_extensions = v3_req | ||
| 477 | prompt = no | ||
| 478 | |||
| 479 | [req_distinguished_name] | ||
| 480 | CN = $registry_host | ||
| 481 | O = Yocto Project | ||
| 482 | |||
| 483 | [v3_req] | ||
| 484 | keyUsage = keyEncipherment, dataEncipherment | ||
| 485 | extendedKeyUsage = serverAuth | ||
| 486 | subjectAltName = $san_list | ||
| 487 | SSLEOF | ||
| 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 | ||
| 521 | setup_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 | ||
| 563 | get_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) | ||
| 589 | get_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 | ||
| 598 | get_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) | ||
| 605 | get_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 | ||
| 615 | get_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 | ||
| 694 | generate_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 | |||
| 243 | cmd_start() {{ | 736 | cmd_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 | ||
| 276 | cmd_stop() {{ | 811 | cmd_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 | ||
| 294 | cmd_status() {{ | 835 | cmd_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() {{ | |||
| 342 | is_manifest_list() {{ | 903 | is_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() {{ | |||
| 355 | get_manifest_list() {{ | 919 | get_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() {{ | |||
| 366 | get_manifest_info() {{ | 933 | get_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] |
| 381 | push_by_digest() {{ | 951 | push_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"}}}} |
| 451 | MANIFEST | 1039 | MANIFEST |
| @@ -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 " | ||
| 469 | import sys, json | 1064 | import sys, json |
| 470 | manifests = [] | 1065 | manifests = [] |
| 471 | for line in sys.stdin: | 1066 | for 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) | ||
| 1075 | if not manifests: | ||
| 1076 | print('ERROR: No valid manifests to create list', file=sys.stderr) | ||
| 1077 | sys.exit(1) | ||
| 475 | result = {{ | 1078 | result = {{ |
| 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 = {{ | |||
| 480 | print(json.dumps(result, indent=2)) | 1083 | print(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 | ||
| 708 | cmd_catalog() {{ | 1364 | cmd_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 | ||
| 713 | cmd_tags() {{ | 1372 | cmd_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 | ||
| 752 | cmd_list() {{ | 1414 | cmd_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 | ||
| 778 | cmd_import() {{ | 1444 | cmd_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 | ||
| 1019 | case "${{1:-help}}" in | 1805 | case "${{1:-help}}" in |
| @@ -1052,7 +1838,7 @@ esac | |||
| 1052 | bb.plain("") | 1838 | bb.plain("") |
| 1053 | } | 1839 | } |
| 1054 | 1840 | ||
| 1055 | do_generate_registry_script[depends] += "docker-distribution-native:do_populate_sysroot skopeo-native:do_populate_sysroot" | 1841 | do_generate_registry_script[depends] += "docker-distribution-native:do_populate_sysroot skopeo-native:do_populate_sysroot openssl-native:do_populate_sysroot" |
| 1056 | addtask do_generate_registry_script | 1842 | addtask do_generate_registry_script |
| 1057 | 1843 | ||
| 1058 | EXCLUDE_FROM_WORLD = "1" | 1844 | EXCLUDE_FROM_WORLD = "1" |
