diff options
Diffstat (limited to 'recipes-containers/vcontainer')
12 files changed, 696 insertions, 76 deletions
diff --git a/recipes-containers/vcontainer/README.md b/recipes-containers/vcontainer/README.md index 76d54d18..e44616f4 100644 --- a/recipes-containers/vcontainer/README.md +++ b/recipes-containers/vcontainer/README.md | |||
| @@ -268,6 +268,109 @@ vdkr pull alpine:latest | |||
| 268 | vdkr images # Shows alpine:latest | 268 | vdkr images # Shows alpine:latest |
| 269 | ``` | 269 | ``` |
| 270 | 270 | ||
| 271 | ## Registry Login and Configuration | ||
| 272 | |||
| 273 | For authenticated registries: | ||
| 274 | |||
| 275 | ```bash | ||
| 276 | # Login (interactive password prompt) | ||
| 277 | vdkr login --username myuser https://registry.example.com/ | ||
| 278 | |||
| 279 | # Pull from the registry | ||
| 280 | vdkr pull registry.example.com/myimage:latest | ||
| 281 | ``` | ||
| 282 | |||
| 283 | Set a default registry so you don't need to specify the full URL each time: | ||
| 284 | |||
| 285 | ```bash | ||
| 286 | # Set default registry (persisted across sessions) | ||
| 287 | vdkr vconfig registry registry.example.com | ||
| 288 | |||
| 289 | # Now pulls try the default registry first, then Docker Hub | ||
| 290 | vdkr pull myimage:latest | ||
| 291 | |||
| 292 | # One-off override without changing the default | ||
| 293 | vdkr --registry other.registry.com pull myimage:latest | ||
| 294 | |||
| 295 | # Clear default registry | ||
| 296 | vdkr vconfig registry --reset | ||
| 297 | ``` | ||
| 298 | |||
| 299 | **Note:** The `--registry` flag is a vdkr option that sets the default | ||
| 300 | registry for pulls. For `login`, pass the registry URL as a positional | ||
| 301 | argument after the login flags: | ||
| 302 | |||
| 303 | ```bash | ||
| 304 | # Correct: | ||
| 305 | vdkr login --username myuser https://registry.example.com/ | ||
| 306 | |||
| 307 | # Wrong (--registry is consumed by vdkr, login gets no URL): | ||
| 308 | vdkr --registry https://registry.example.com/ login --username myuser | ||
| 309 | ``` | ||
| 310 | |||
| 311 | **TLS certificates:** The vdkr/vpdmn rootfs images include common | ||
| 312 | intermediate certificates (Let's Encrypt E8/R11) to handle registries | ||
| 313 | that don't send the full certificate chain. For self-signed registries, | ||
| 314 | use `--secure-registry --ca-cert`: | ||
| 315 | |||
| 316 | ```bash | ||
| 317 | vdkr --secure-registry --ca-cert /path/to/ca.crt pull myimage | ||
| 318 | ``` | ||
| 319 | |||
| 320 | ### Passing an existing docker/podman auth file (`--config`) | ||
| 321 | |||
| 322 | If you already have credentials set up on the host (for example, from | ||
| 323 | running `docker login` locally), you can pass the resulting auth file | ||
| 324 | straight through into the emulated environment instead of re-entering | ||
| 325 | credentials with `--registry-user`/`--registry-pass`: | ||
| 326 | |||
| 327 | ```bash | ||
| 328 | # Docker (vdkr): uses ~/.docker/config.json by default | ||
| 329 | vdkr --config ~/.docker/config.json pull registry.example.com/myimage | ||
| 330 | |||
| 331 | # Podman (vpdmn): uses $XDG_RUNTIME_DIR/containers/auth.json | ||
| 332 | vpdmn --config $XDG_RUNTIME_DIR/containers/auth.json pull registry.example.com/myimage | ||
| 333 | ``` | ||
| 334 | |||
| 335 | The path can also be supplied via environment: | ||
| 336 | |||
| 337 | ```bash | ||
| 338 | export VDKR_CONFIG=$HOME/.docker/config.json | ||
| 339 | vdkr pull registry.example.com/myimage | ||
| 340 | ``` | ||
| 341 | |||
| 342 | (`VPDMN_CONFIG` is honoured identically by `vpdmn`.) | ||
| 343 | |||
| 344 | **What the file ends up as inside the VM:** | ||
| 345 | |||
| 346 | | Tool | Target path | Notes | | ||
| 347 | | ----- | --------------------------------- | ------------------------------------------------- | | ||
| 348 | | vdkr | `/root/.docker/config.json` | Mode 0600; containing dir 0700 | | ||
| 349 | | vpdmn | `/run/containers/0/auth.json` | Mode 0600; `$REGISTRY_AUTH_FILE` exported | | ||
| 350 | |||
| 351 | **Security model.** The credential file is treated as secret material: | ||
| 352 | |||
| 353 | - The host-side file **must** be a regular file with mode `0600` or `0400`. | ||
| 354 | World/group-readable files are rejected outright. Symlinks are rejected. | ||
| 355 | Files larger than 1 MiB are rejected. | ||
| 356 | - On the host it is copied into a per-invocation private directory under | ||
| 357 | `$TMPDIR/vdkr-$$/auth_share` (mode 0700; file mode 0400) and removed | ||
| 358 | automatically by the `EXIT`/`INT`/`TERM` trap when `vrunner.sh` exits. | ||
| 359 | - It is exposed to the guest on a **dedicated** virtio-9p share whose | ||
| 360 | mount tag (`vdkr_auth` / `vpdmn_auth`) is distinct from the general | ||
| 361 | `*_share` share used for input/output. The guest mounts it **read-only** | ||
| 362 | at `/mnt/auth`, copies it into the runtime's credential location, then | ||
| 363 | **unmounts** `/mnt/auth` so nothing in the VM retains an open reference | ||
| 364 | to the host staging directory. | ||
| 365 | - Nothing about the file appears on the kernel command line. Only a | ||
| 366 | boolean flag (`docker_auth=1` / `podman_auth=1`) is passed so the guest | ||
| 367 | init script knows to look on the auth share. | ||
| 368 | - When both `--config` and `--registry-user`/`--registry-pass` are | ||
| 369 | supplied, `--config` wins and a NOTE is logged. | ||
| 370 | - `--config` is NOT forwarded into container workloads (it only reaches | ||
| 371 | the container engine's credential store); containers themselves never | ||
| 372 | see `/mnt/auth`. | ||
| 373 | |||
| 271 | ## Volume Mounts | 374 | ## Volume Mounts |
| 272 | 375 | ||
| 273 | Mount host directories into containers using `-v` (requires memory resident mode): | 376 | Mount host directories into containers using `-v` (requires memory resident mode): |
diff --git a/recipes-containers/vcontainer/files/vcontainer-common.sh b/recipes-containers/vcontainer/files/vcontainer-common.sh index c7160860..f389941c 100755 --- a/recipes-containers/vcontainer/files/vcontainer-common.sh +++ b/recipes-containers/vcontainer/files/vcontainer-common.sh | |||
| @@ -327,40 +327,54 @@ normalize_arch_from_oci() { | |||
| 327 | esac | 327 | esac |
| 328 | } | 328 | } |
| 329 | 329 | ||
| 330 | # Check if OCI directory contains a multi-architecture Image Index | 330 | # Resolve the file containing platform manifests in an OCI directory. |
| 331 | # Usage: is_oci_image_index <oci_dir> | 331 | # Handles two layouts: |
| 332 | # Returns: 0 if multi-arch, 1 if single-arch or not OCI | 332 | # Flat: index.json directly contains manifests with platform info |
| 333 | is_oci_image_index() { | 333 | # Nested: index.json → single image index blob → platform manifests |
| 334 | # (skopeo-compatible format) | ||
| 335 | # Usage: _resolve_oci_platform_file <oci_dir> | ||
| 336 | # Prints: path to the file containing platform manifests, or empty | ||
| 337 | _resolve_oci_platform_file() { | ||
| 334 | local oci_dir="$1" | 338 | local oci_dir="$1" |
| 335 | 339 | ||
| 336 | [ -f "$oci_dir/index.json" ] || return 1 | 340 | [ -f "$oci_dir/index.json" ] || return 1 |
| 337 | 341 | ||
| 338 | # Check if index.json has manifests with platform info | 342 | # Flat layout: platform info directly in index.json |
| 339 | # Multi-arch images have "platform" object in manifest entries | ||
| 340 | if grep -q '"platform"' "$oci_dir/index.json" 2>/dev/null; then | 343 | if grep -q '"platform"' "$oci_dir/index.json" 2>/dev/null; then |
| 341 | # Also verify there are multiple manifests | 344 | echo "$oci_dir/index.json" |
| 342 | local manifest_count=$(grep -c '"digest"' "$oci_dir/index.json" 2>/dev/null || echo "0") | ||
| 343 | [ "$manifest_count" -gt 1 ] && return 0 | ||
| 344 | |||
| 345 | # Single manifest with platform info is also a valid Image Index | ||
| 346 | # (could be a multi-arch image built with only one arch so far) | ||
| 347 | return 0 | 345 | return 0 |
| 348 | fi | 346 | fi |
| 349 | 347 | ||
| 348 | # Nested layout: index.json has a single entry with image.index mediaType | ||
| 349 | if grep -q 'image\.index' "$oci_dir/index.json" 2>/dev/null; then | ||
| 350 | local index_digest=$(grep -o '"sha256:[a-f0-9]*"' "$oci_dir/index.json" 2>/dev/null | head -1 | tr -d '"' | sed 's/sha256://') | ||
| 351 | if [ -n "$index_digest" ] && [ -f "$oci_dir/blobs/sha256/$index_digest" ]; then | ||
| 352 | if grep -q '"platform"' "$oci_dir/blobs/sha256/$index_digest" 2>/dev/null; then | ||
| 353 | echo "$oci_dir/blobs/sha256/$index_digest" | ||
| 354 | return 0 | ||
| 355 | fi | ||
| 356 | fi | ||
| 357 | fi | ||
| 358 | |||
| 350 | return 1 | 359 | return 1 |
| 351 | } | 360 | } |
| 352 | 361 | ||
| 362 | # Check if OCI directory contains a multi-architecture Image Index | ||
| 363 | # Usage: is_oci_image_index <oci_dir> | ||
| 364 | # Returns: 0 if multi-arch, 1 if single-arch or not OCI | ||
| 365 | is_oci_image_index() { | ||
| 366 | _resolve_oci_platform_file "$1" >/dev/null 2>&1 | ||
| 367 | } | ||
| 368 | |||
| 353 | # Get list of available platforms in a multi-arch OCI Image Index | 369 | # Get list of available platforms in a multi-arch OCI Image Index |
| 354 | # Usage: get_oci_platforms <oci_dir> | 370 | # Usage: get_oci_platforms <oci_dir> |
| 355 | # Returns: Space-separated list of architectures (e.g., "arm64 amd64") | 371 | # Returns: Space-separated list of architectures (e.g., "arm64 amd64") |
| 356 | get_oci_platforms() { | 372 | get_oci_platforms() { |
| 357 | local oci_dir="$1" | 373 | local oci_dir="$1" |
| 374 | local platform_file | ||
| 375 | platform_file=$(_resolve_oci_platform_file "$oci_dir") || return 1 | ||
| 358 | 376 | ||
| 359 | [ -f "$oci_dir/index.json" ] || return 1 | 377 | grep -o '"architecture"[[:space:]]*:[[:space:]]*"[^"]*"' "$platform_file" 2>/dev/null | \ |
| 360 | |||
| 361 | # Extract architecture values from platform objects | ||
| 362 | # Format: "platform": { "architecture": "arm64", "os": "linux" } | ||
| 363 | grep -o '"architecture"[[:space:]]*:[[:space:]]*"[^"]*"' "$oci_dir/index.json" 2>/dev/null | \ | ||
| 364 | sed 's/.*"\([^"]*\)"$/\1/' | \ | 378 | sed 's/.*"\([^"]*\)"$/\1/' | \ |
| 365 | tr '\n' ' ' | sed 's/ $//' | 379 | tr '\n' ' ' | sed 's/ $//' |
| 366 | } | 380 | } |
| @@ -373,38 +387,34 @@ select_platform_manifest() { | |||
| 373 | local oci_dir="$1" | 387 | local oci_dir="$1" |
| 374 | local target_arch="$2" | 388 | local target_arch="$2" |
| 375 | 389 | ||
| 376 | [ -f "$oci_dir/index.json" ] || return 1 | ||
| 377 | |||
| 378 | # Normalize target arch to OCI convention | 390 | # Normalize target arch to OCI convention |
| 379 | local oci_arch=$(normalize_arch_to_oci "$target_arch") | 391 | local oci_arch=$(normalize_arch_to_oci "$target_arch") |
| 380 | 392 | ||
| 381 | # Parse index.json to find manifest with matching platform | 393 | # Resolve the file containing platform manifests (flat or nested) |
| 394 | local manifest_index | ||
| 395 | manifest_index=$(_resolve_oci_platform_file "$oci_dir") || return 1 | ||
| 396 | |||
| 397 | # Parse the manifest index to find manifest with matching platform | ||
| 382 | # This is done without jq using grep/sed for portability | 398 | # This is done without jq using grep/sed for portability |
| 383 | local in_manifest=0 | 399 | local in_manifest=0 |
| 384 | local current_digest="" | 400 | local current_digest="" |
| 385 | local current_arch="" | 401 | local current_arch="" |
| 386 | local matched_digest="" | 402 | local matched_digest="" |
| 387 | 403 | ||
| 388 | # Read index.json line by line | ||
| 389 | while IFS= read -r line; do | 404 | while IFS= read -r line; do |
| 390 | # Track when we're inside a manifest entry | ||
| 391 | if echo "$line" | grep -q '"manifests"'; then | 405 | if echo "$line" | grep -q '"manifests"'; then |
| 392 | in_manifest=1 | 406 | in_manifest=1 |
| 393 | continue | 407 | continue |
| 394 | fi | 408 | fi |
| 395 | 409 | ||
| 396 | if [ "$in_manifest" = "1" ]; then | 410 | if [ "$in_manifest" = "1" ]; then |
| 397 | # Extract digest | ||
| 398 | if echo "$line" | grep -q '"digest"'; then | 411 | if echo "$line" | grep -q '"digest"'; then |
| 399 | current_digest=$(echo "$line" | sed 's/.*"sha256:\([a-f0-9]*\)".*/\1/') | 412 | current_digest=$(echo "$line" | sed 's/.*"sha256:\([a-f0-9]*\)".*/\1/') |
| 400 | fi | 413 | fi |
| 401 | 414 | ||
| 402 | # Extract architecture from platform | ||
| 403 | # Handle both formats: "architecture": "arm64" or {"architecture": "arm64", ...} | ||
| 404 | if echo "$line" | grep -q '"architecture"'; then | 415 | if echo "$line" | grep -q '"architecture"'; then |
| 405 | current_arch=$(echo "$line" | sed 's/.*"architecture"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/') | 416 | current_arch=$(echo "$line" | sed 's/.*"architecture"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/') |
| 406 | 417 | ||
| 407 | # Check if this matches our target | ||
| 408 | if [ "$current_arch" = "$oci_arch" ]; then | 418 | if [ "$current_arch" = "$oci_arch" ]; then |
| 409 | matched_digest="$current_digest" | 419 | matched_digest="$current_digest" |
| 410 | OCI_SELECTED_PLATFORM="$current_arch" | 420 | OCI_SELECTED_PLATFORM="$current_arch" |
| @@ -412,13 +422,12 @@ select_platform_manifest() { | |||
| 412 | fi | 422 | fi |
| 413 | fi | 423 | fi |
| 414 | 424 | ||
| 415 | # Reset on closing brace (end of manifest entry) | ||
| 416 | if echo "$line" | grep -q '^[[:space:]]*}'; then | 425 | if echo "$line" | grep -q '^[[:space:]]*}'; then |
| 417 | current_digest="" | 426 | current_digest="" |
| 418 | current_arch="" | 427 | current_arch="" |
| 419 | fi | 428 | fi |
| 420 | fi | 429 | fi |
| 421 | done < "$oci_dir/index.json" | 430 | done < "$manifest_index" |
| 422 | 431 | ||
| 423 | if [ -n "$matched_digest" ]; then | 432 | if [ -n "$matched_digest" ]; then |
| 424 | echo "$matched_digest" | 433 | echo "$matched_digest" |
| @@ -718,6 +727,11 @@ ${BOLD}GLOBAL OPTIONS:${NC} | |||
| 718 | --registry <url> Default registry for unqualified images (e.g., 10.0.2.2:5000/yocto) | 727 | --registry <url> Default registry for unqualified images (e.g., 10.0.2.2:5000/yocto) |
| 719 | --no-registry Disable baked-in default registry (use images as-is) | 728 | --no-registry Disable baked-in default registry (use images as-is) |
| 720 | --insecure-registry <host:port> Mark registry as insecure (HTTP). Can repeat. | 729 | --insecure-registry <host:port> Mark registry as insecure (HTTP). Can repeat. |
| 730 | --config <path> Registry auth file (docker config.json / podman auth.json) | ||
| 731 | Defaults to \$VDKR_CONFIG / \$VPDMN_CONFIG. The file must be | ||
| 732 | mode 0600 or stricter; it is passed to the guest over a | ||
| 733 | dedicated read-only virtio-9p share and never appears on | ||
| 734 | the kernel cmdline. | ||
| 721 | --verbose, -v Enable verbose output | 735 | --verbose, -v Enable verbose output |
| 722 | --help, -h Show this help | 736 | --help, -h Show this help |
| 723 | 737 | ||
| @@ -857,6 +871,7 @@ build_runner_args() { | |||
| 857 | [ -n "$CA_CERT" ] && args+=("--ca-cert" "$CA_CERT") | 871 | [ -n "$CA_CERT" ] && args+=("--ca-cert" "$CA_CERT") |
| 858 | [ -n "$REGISTRY_USER" ] && args+=("--registry-user" "$REGISTRY_USER") | 872 | [ -n "$REGISTRY_USER" ] && args+=("--registry-user" "$REGISTRY_USER") |
| 859 | [ -n "$REGISTRY_PASS" ] && args+=("--registry-pass" "$REGISTRY_PASS") | 873 | [ -n "$REGISTRY_PASS" ] && args+=("--registry-pass" "$REGISTRY_PASS") |
| 874 | [ -n "$AUTH_CONFIG" ] && args+=("--config" "$AUTH_CONFIG") | ||
| 860 | 875 | ||
| 861 | # Xen: pass exit grace period | 876 | # Xen: pass exit grace period |
| 862 | [ -n "${VXN_EXIT_GRACE_PERIOD:-}" ] && args+=("--exit-grace-period" "$VXN_EXIT_GRACE_PERIOD") | 877 | [ -n "${VXN_EXIT_GRACE_PERIOD:-}" ] && args+=("--exit-grace-period" "$VXN_EXIT_GRACE_PERIOD") |
| @@ -880,6 +895,11 @@ SECURE_REGISTRY="false" | |||
| 880 | CA_CERT="" | 895 | CA_CERT="" |
| 881 | REGISTRY_USER="" | 896 | REGISTRY_USER="" |
| 882 | REGISTRY_PASS="" | 897 | REGISTRY_PASS="" |
| 898 | # Registry auth config file. Env-var default depends on which CLI wrapper is | ||
| 899 | # in use (vdkr → $VDKR_CONFIG, vpdmn → $VPDMN_CONFIG), then falls back to the | ||
| 900 | # other for convenience when sharing a single host-side file. Overridden by | ||
| 901 | # the --config CLI flag below. | ||
| 902 | AUTH_CONFIG="${VDKR_CONFIG:-${VPDMN_CONFIG:-}}" | ||
| 883 | COMMAND="" | 903 | COMMAND="" |
| 884 | COMMAND_ARGS=() | 904 | COMMAND_ARGS=() |
| 885 | 905 | ||
| @@ -977,6 +997,13 @@ while [ $# -gt 0 ]; do | |||
| 977 | REGISTRY_PASS="$2" | 997 | REGISTRY_PASS="$2" |
| 978 | shift 2 | 998 | shift 2 |
| 979 | ;; | 999 | ;; |
| 1000 | --config) | ||
| 1001 | # Path to a docker/podman registry auth file (config.json / auth.json). | ||
| 1002 | # Overrides $VDKR_CONFIG / $VPDMN_CONFIG. Forwarded to vrunner.sh --config, | ||
| 1003 | # which validates the file and stages it on a dedicated read-only 9p share. | ||
| 1004 | AUTH_CONFIG="$2" | ||
| 1005 | shift 2 | ||
| 1006 | ;; | ||
| 980 | -it|--interactive) | 1007 | -it|--interactive) |
| 981 | INTERACTIVE="true" | 1008 | INTERACTIVE="true" |
| 982 | shift | 1009 | shift |
| @@ -1858,11 +1885,11 @@ case "$COMMAND" in | |||
| 1858 | 1885 | ||
| 1859 | # Check for multi-architecture OCI Image Index | 1886 | # Check for multi-architecture OCI Image Index |
| 1860 | if is_oci_image_index "$INPUT_PATH"; then | 1887 | if is_oci_image_index "$INPUT_PATH"; then |
| 1861 | local available_platforms=$(get_oci_platforms "$INPUT_PATH") | 1888 | available_platforms=$(get_oci_platforms "$INPUT_PATH") |
| 1862 | [ "$VERBOSE" = "true" ] && echo -e "${CYAN}[$VCONTAINER_RUNTIME_NAME]${NC} Multi-arch OCI detected. Available: $available_platforms" >&2 | 1889 | [ "$VERBOSE" = "true" ] && echo -e "${CYAN}[$VCONTAINER_RUNTIME_NAME]${NC} Multi-arch OCI detected. Available: $available_platforms" >&2 |
| 1863 | 1890 | ||
| 1864 | # Select manifest for target architecture | 1891 | # Select manifest for target architecture |
| 1865 | local manifest_digest=$(select_platform_manifest "$INPUT_PATH" "$TARGET_ARCH") | 1892 | manifest_digest=$(select_platform_manifest "$INPUT_PATH" "$TARGET_ARCH") |
| 1866 | if [ -z "$manifest_digest" ]; then | 1893 | if [ -z "$manifest_digest" ]; then |
| 1867 | echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} Architecture $TARGET_ARCH not found in multi-arch image" >&2 | 1894 | echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} Architecture $TARGET_ARCH not found in multi-arch image" >&2 |
| 1868 | echo -e "${YELLOW}[$VCONTAINER_RUNTIME_NAME]${NC} Available platforms: $available_platforms" >&2 | 1895 | echo -e "${YELLOW}[$VCONTAINER_RUNTIME_NAME]${NC} Available platforms: $available_platforms" >&2 |
| @@ -1870,7 +1897,7 @@ case "$COMMAND" in | |||
| 1870 | exit 1 | 1897 | exit 1 |
| 1871 | fi | 1898 | fi |
| 1872 | 1899 | ||
| 1873 | echo -e "${GREEN}[$VCONTAINER_RUNTIME_NAME]${NC} Selected platform: $OCI_SELECTED_PLATFORM (from multi-arch image)" >&2 | 1900 | echo -e "${GREEN}[$VCONTAINER_RUNTIME_NAME]${NC} Selected platform: $(normalize_arch_to_oci "$TARGET_ARCH")/linux (from multi-arch image)" >&2 |
| 1874 | 1901 | ||
| 1875 | # Extract single-platform OCI to temp directory | 1902 | # Extract single-platform OCI to temp directory |
| 1876 | TEMP_OCI_DIR=$(mktemp -d) | 1903 | TEMP_OCI_DIR=$(mktemp -d) |
| @@ -2196,9 +2223,14 @@ case "$COMMAND" in | |||
| 2196 | 2223 | ||
| 2197 | login) | 2224 | login) |
| 2198 | [ "${VCONTAINER_HYPERVISOR:-}" = "xen" ] && vxn_unsupported "login" | 2225 | [ "${VCONTAINER_HYPERVISOR:-}" = "xen" ] && vxn_unsupported "login" |
| 2199 | # Login to registry - may need credentials via stdin | 2226 | # Login needs interactive stdin for password prompt. |
| 2200 | # For non-interactive: runtime login -u user -p pass registry | 2227 | # Use daemon-interactive mode (same as vshell/exec -it). |
| 2201 | run_runtime_command "$VCONTAINER_RUNTIME_CMD login ${COMMAND_ARGS[*]}" | 2228 | if daemon_is_running; then |
| 2229 | RUNNER_ARGS=$(build_runner_args) | ||
| 2230 | "$RUNNER" $RUNNER_ARGS --daemon-interactive -- "$VCONTAINER_RUNTIME_CMD login ${COMMAND_ARGS[*]}" | ||
| 2231 | else | ||
| 2232 | run_runtime_command "$VCONTAINER_RUNTIME_CMD login ${COMMAND_ARGS[*]}" | ||
| 2233 | fi | ||
| 2202 | ;; | 2234 | ;; |
| 2203 | 2235 | ||
| 2204 | logout) | 2236 | logout) |
| @@ -2483,12 +2515,20 @@ case "$COMMAND" in | |||
| 2483 | STORAGE_CMD="${COMMAND_ARGS[0]}" | 2515 | STORAGE_CMD="${COMMAND_ARGS[0]}" |
| 2484 | fi | 2516 | fi |
| 2485 | 2517 | ||
| 2518 | # When --state-dir is passed, scan its parent as the storage root | ||
| 2519 | # (STATE_DIR is an arch subdir like ~/.vpdmn-test/x86_64, so the | ||
| 2520 | # parent ~/.vpdmn-test/ is the root containing all arch dirs). | ||
| 2521 | VSTORAGE_ROOT="$DEFAULT_STATE_DIR" | ||
| 2522 | if [ -n "$STATE_DIR" ]; then | ||
| 2523 | VSTORAGE_ROOT="$(dirname "$STATE_DIR")" | ||
| 2524 | fi | ||
| 2525 | |||
| 2486 | case "$STORAGE_CMD" in | 2526 | case "$STORAGE_CMD" in |
| 2487 | list) | 2527 | list) |
| 2488 | echo "$VCONTAINER_RUNTIME_NAME storage directories:" | 2528 | echo "$VCONTAINER_RUNTIME_NAME storage directories:" |
| 2489 | echo "" | 2529 | echo "" |
| 2490 | found=0 | 2530 | found=0 |
| 2491 | for state_dir in "$DEFAULT_STATE_DIR"/*/; do | 2531 | for state_dir in "$VSTORAGE_ROOT"/*/; do |
| 2492 | [ -d "$state_dir" ] || continue | 2532 | [ -d "$state_dir" ] || continue |
| 2493 | found=1 | 2533 | found=1 |
| 2494 | instance=$(basename "$state_dir") | 2534 | instance=$(basename "$state_dir") |
| @@ -2517,8 +2557,8 @@ case "$COMMAND" in | |||
| 2517 | fi | 2557 | fi |
| 2518 | 2558 | ||
| 2519 | # Total size | 2559 | # Total size |
| 2520 | if [ -d "$DEFAULT_STATE_DIR" ] && [ $found -gt 0 ]; then | 2560 | if [ -d "$VSTORAGE_ROOT" ] && [ $found -gt 0 ]; then |
| 2521 | total=$(du -sh "$DEFAULT_STATE_DIR" 2>/dev/null | cut -f1) | 2561 | total=$(du -sh "$VSTORAGE_ROOT" 2>/dev/null | cut -f1) |
| 2522 | echo "Total: $total" | 2562 | echo "Total: $total" |
| 2523 | fi | 2563 | fi |
| 2524 | ;; | 2564 | ;; |
| @@ -2531,7 +2571,7 @@ case "$COMMAND" in | |||
| 2531 | 2571 | ||
| 2532 | df) | 2572 | df) |
| 2533 | # Detailed breakdown | 2573 | # Detailed breakdown |
| 2534 | for state_dir in "$DEFAULT_STATE_DIR"/*/; do | 2574 | for state_dir in "$VSTORAGE_ROOT"/*/; do |
| 2535 | [ -d "$state_dir" ] || continue | 2575 | [ -d "$state_dir" ] || continue |
| 2536 | instance=$(basename "$state_dir") | 2576 | instance=$(basename "$state_dir") |
| 2537 | echo "${BOLD}$instance${NC}:" | 2577 | echo "${BOLD}$instance${NC}:" |
| @@ -2552,7 +2592,7 @@ case "$COMMAND" in | |||
| 2552 | arch="${COMMAND_ARGS[1]:-}" | 2592 | arch="${COMMAND_ARGS[1]:-}" |
| 2553 | if [ "$arch" = "--all" ]; then | 2593 | if [ "$arch" = "--all" ]; then |
| 2554 | # Stop any running memres first | 2594 | # Stop any running memres first |
| 2555 | for pid_file in "$DEFAULT_STATE_DIR"/*/daemon.pid; do | 2595 | for pid_file in "$VSTORAGE_ROOT"/*/daemon.pid; do |
| 2556 | [ -f "$pid_file" ] || continue | 2596 | [ -f "$pid_file" ] || continue |
| 2557 | pid=$(cat "$pid_file" 2>/dev/null) | 2597 | pid=$(cat "$pid_file" 2>/dev/null) |
| 2558 | if [ -n "$pid" ] && [ -d "/proc/$pid" ]; then | 2598 | if [ -n "$pid" ] && [ -d "/proc/$pid" ]; then |
| @@ -2561,11 +2601,11 @@ case "$COMMAND" in | |||
| 2561 | fi | 2601 | fi |
| 2562 | done | 2602 | done |
| 2563 | echo -e "${YELLOW}[$VCONTAINER_RUNTIME_NAME]${NC} Removing all storage directories..." | 2603 | echo -e "${YELLOW}[$VCONTAINER_RUNTIME_NAME]${NC} Removing all storage directories..." |
| 2564 | rm -rf "$DEFAULT_STATE_DIR" | 2604 | rm -rf "$VSTORAGE_ROOT" |
| 2565 | echo -e "${GREEN}[$VCONTAINER_RUNTIME_NAME]${NC} All storage cleaned." | 2605 | echo -e "${GREEN}[$VCONTAINER_RUNTIME_NAME]${NC} All storage cleaned." |
| 2566 | elif [ -n "$arch" ]; then | 2606 | elif [ -n "$arch" ]; then |
| 2567 | # Clean specific arch | 2607 | # Clean specific arch |
| 2568 | clean_dir="$DEFAULT_STATE_DIR/$arch" | 2608 | clean_dir="$VSTORAGE_ROOT/$arch" |
| 2569 | if [ -d "$clean_dir" ]; then | 2609 | if [ -d "$clean_dir" ]; then |
| 2570 | # Stop memres if running | 2610 | # Stop memres if running |
| 2571 | if [ -f "$clean_dir/daemon.pid" ]; then | 2611 | if [ -f "$clean_dir/daemon.pid" ]; then |
diff --git a/recipes-containers/vcontainer/files/vcontainer-init-common.sh b/recipes-containers/vcontainer/files/vcontainer-init-common.sh index ab8762b2..3bd70e75 100755 --- a/recipes-containers/vcontainer/files/vcontainer-init-common.sh +++ b/recipes-containers/vcontainer/files/vcontainer-init-common.sh | |||
| @@ -156,6 +156,7 @@ parse_cmdline() { | |||
| 156 | RUNTIME_INTERACTIVE="0" | 156 | RUNTIME_INTERACTIVE="0" |
| 157 | RUNTIME_DAEMON="0" | 157 | RUNTIME_DAEMON="0" |
| 158 | RUNTIME_9P="0" # virtio-9p available for fast I/O | 158 | RUNTIME_9P="0" # virtio-9p available for fast I/O |
| 159 | RUNTIME_AUTH="0" # registry auth config (config.json / auth.json) available on dedicated 9p share | ||
| 159 | RUNTIME_IDLE_TIMEOUT="1800" # Default: 30 minutes | 160 | RUNTIME_IDLE_TIMEOUT="1800" # Default: 30 minutes |
| 160 | 161 | ||
| 161 | for param in $(cat /proc/cmdline); do | 162 | for param in $(cat /proc/cmdline); do |
| @@ -187,6 +188,9 @@ parse_cmdline() { | |||
| 187 | ${VCONTAINER_RUNTIME_PREFIX}_9p=*) | 188 | ${VCONTAINER_RUNTIME_PREFIX}_9p=*) |
| 188 | RUNTIME_9P="${param#${VCONTAINER_RUNTIME_PREFIX}_9p=}" | 189 | RUNTIME_9P="${param#${VCONTAINER_RUNTIME_PREFIX}_9p=}" |
| 189 | ;; | 190 | ;; |
| 191 | ${VCONTAINER_RUNTIME_PREFIX}_auth=*) | ||
| 192 | RUNTIME_AUTH="${param#${VCONTAINER_RUNTIME_PREFIX}_auth=}" | ||
| 193 | ;; | ||
| 190 | esac | 194 | esac |
| 191 | done | 195 | done |
| 192 | 196 | ||
| @@ -263,6 +267,56 @@ mount_input_disk() { | |||
| 263 | } | 267 | } |
| 264 | 268 | ||
| 265 | # ============================================================================ | 269 | # ============================================================================ |
| 270 | # Registry auth share (docker config.json / podman auth.json) | ||
| 271 | # ============================================================================ | ||
| 272 | # The host stages a validated credential file on a *dedicated* read-only 9p | ||
| 273 | # share tagged "${VCONTAINER_RUNTIME_NAME}_auth" (e.g. "vdkr_auth" or | ||
| 274 | # "vpdmn_auth"). That tag is separate from the general ${VCONTAINER_SHARE_NAME} | ||
| 275 | # used for input/output so credentials can't leak into storage.tar outputs or | ||
| 276 | # be overwritten by daemon_send_with_input. | ||
| 277 | # | ||
| 278 | # We mount read-only, nosuid, nodev, noexec at /mnt/auth. Callers are expected | ||
| 279 | # to copy the credential file into the runtime's canonical location with | ||
| 280 | # restrictive permissions and then call unmount_auth_share() so the guest | ||
| 281 | # filesystem no longer has an open reference to the host-side file. | ||
| 282 | |||
| 283 | AUTH_SHARE_TAG="" | ||
| 284 | AUTH_SHARE_MOUNT="/mnt/auth" | ||
| 285 | |||
| 286 | mount_auth_share() { | ||
| 287 | if [ "$RUNTIME_AUTH" != "1" ]; then | ||
| 288 | return 1 | ||
| 289 | fi | ||
| 290 | |||
| 291 | AUTH_SHARE_TAG="${VCONTAINER_RUNTIME_NAME}_auth" | ||
| 292 | mkdir -p "$AUTH_SHARE_MOUNT" | ||
| 293 | |||
| 294 | # trans/version/cache match the existing 9p share mount. Add: | ||
| 295 | # ro - guest can't mutate the host-side staging directory | ||
| 296 | # nosuid - no setuid binaries can be executed from the share | ||
| 297 | # nodev - no device nodes honoured even if crafted | ||
| 298 | # noexec - no code can execute from the share (auth.json is pure data) | ||
| 299 | if mount -t 9p \ | ||
| 300 | -o trans=${NINE_P_TRANSPORT},version=9p2000.L,cache=none,ro,nosuid,nodev,noexec \ | ||
| 301 | "$AUTH_SHARE_TAG" "$AUTH_SHARE_MOUNT" 2>/dev/null; then | ||
| 302 | log "Mounted auth 9p share at $AUTH_SHARE_MOUNT (tag: $AUTH_SHARE_TAG, ro)" | ||
| 303 | return 0 | ||
| 304 | fi | ||
| 305 | |||
| 306 | log "WARNING: Could not mount auth 9p share ($AUTH_SHARE_TAG)" | ||
| 307 | RUNTIME_AUTH="0" | ||
| 308 | return 1 | ||
| 309 | } | ||
| 310 | |||
| 311 | unmount_auth_share() { | ||
| 312 | if mountpoint -q "$AUTH_SHARE_MOUNT" 2>/dev/null; then | ||
| 313 | umount "$AUTH_SHARE_MOUNT" 2>/dev/null || \ | ||
| 314 | umount -l "$AUTH_SHARE_MOUNT" 2>/dev/null || true | ||
| 315 | fi | ||
| 316 | rmdir "$AUTH_SHARE_MOUNT" 2>/dev/null || true | ||
| 317 | } | ||
| 318 | |||
| 319 | # ============================================================================ | ||
| 266 | # Network Configuration | 320 | # Network Configuration |
| 267 | # ============================================================================ | 321 | # ============================================================================ |
| 268 | 322 | ||
diff --git a/recipes-containers/vcontainer/files/vdkr-init.sh b/recipes-containers/vcontainer/files/vdkr-init.sh index e1e869b2..4ad50668 100755 --- a/recipes-containers/vcontainer/files/vdkr-init.sh +++ b/recipes-containers/vcontainer/files/vdkr-init.sh | |||
| @@ -26,6 +26,10 @@ | |||
| 26 | # docker_registry_ca=1 CA certificate available in /mnt/share/ca.crt | 26 | # docker_registry_ca=1 CA certificate available in /mnt/share/ca.crt |
| 27 | # docker_registry_user=<user> Registry username for authentication | 27 | # docker_registry_user=<user> Registry username for authentication |
| 28 | # docker_registry_pass=<base64> Base64-encoded registry password | 28 | # docker_registry_pass=<base64> Base64-encoded registry password |
| 29 | # docker_auth=1 A pre-built docker config.json is available | ||
| 30 | # on a dedicated read-only 9p share tagged | ||
| 31 | # "vdkr_auth" (mounted at /mnt/auth). Takes | ||
| 32 | # precedence over docker_registry_user/pass. | ||
| 29 | # | 33 | # |
| 30 | # Version: 2.5.0 | 34 | # Version: 2.5.0 |
| 31 | 35 | ||
| @@ -159,6 +163,55 @@ EOF | |||
| 159 | fi | 163 | fi |
| 160 | } | 164 | } |
| 161 | 165 | ||
| 166 | # Install a user-supplied docker config.json from the dedicated read-only | ||
| 167 | # auth 9p share (mounted at /mnt/auth by mount_auth_share). This takes | ||
| 168 | # precedence over credentials supplied via docker_registry_user/pass. | ||
| 169 | # | ||
| 170 | # Security posture: | ||
| 171 | # * File is read from a read-only 9p share with a separate tag ("vdkr_auth") | ||
| 172 | # so it cannot leak into /mnt/share outputs. | ||
| 173 | # * Target is written with mode 0600 and the parent dir with mode 0700. | ||
| 174 | # * We unmount /mnt/auth immediately after copying so neither the dockerd | ||
| 175 | # runtime nor user workloads in the VM have an open reference to the | ||
| 176 | # host-side staging directory. | ||
| 177 | install_auth_config() { | ||
| 178 | if [ "$RUNTIME_AUTH" != "1" ]; then | ||
| 179 | return 0 | ||
| 180 | fi | ||
| 181 | |||
| 182 | if ! mount_auth_share; then | ||
| 183 | log "WARNING: docker_auth=1 was set but the auth 9p share did not mount" | ||
| 184 | return 1 | ||
| 185 | fi | ||
| 186 | |||
| 187 | local src="$AUTH_SHARE_MOUNT/config.json" | ||
| 188 | if [ ! -f "$src" ]; then | ||
| 189 | log "WARNING: expected $src on auth share but file is missing" | ||
| 190 | unmount_auth_share | ||
| 191 | return 1 | ||
| 192 | fi | ||
| 193 | |||
| 194 | mkdir -p /root/.docker | ||
| 195 | chmod 700 /root/.docker | ||
| 196 | |||
| 197 | if cp "$src" /root/.docker/config.json 2>/dev/null; then | ||
| 198 | chmod 600 /root/.docker/config.json | ||
| 199 | log "Installed registry auth config at /root/.docker/config.json" | ||
| 200 | if [ -n "$DOCKER_REGISTRY_USER" ] || [ -n "$DOCKER_REGISTRY_PASS" ]; then | ||
| 201 | log "NOTE: --config takes precedence over --registry-user/--registry-pass" | ||
| 202 | fi | ||
| 203 | else | ||
| 204 | log "ERROR: failed to copy auth config to /root/.docker/config.json" | ||
| 205 | unmount_auth_share | ||
| 206 | return 1 | ||
| 207 | fi | ||
| 208 | |||
| 209 | # Release the host-side share so credentials aren't still addressable | ||
| 210 | # through /mnt/auth for the lifetime of the VM. | ||
| 211 | unmount_auth_share | ||
| 212 | return 0 | ||
| 213 | } | ||
| 214 | |||
| 162 | # ============================================================================ | 215 | # ============================================================================ |
| 163 | # Docker-Specific Functions | 216 | # Docker-Specific Functions |
| 164 | # ============================================================================ | 217 | # ============================================================================ |
| @@ -684,6 +737,11 @@ parse_secure_registry_config | |||
| 684 | # Install CA certificate for secure registry | 737 | # Install CA certificate for secure registry |
| 685 | install_registry_ca | 738 | install_registry_ca |
| 686 | 739 | ||
| 740 | # Install user-supplied docker config.json from the dedicated auth 9p share. | ||
| 741 | # Must run AFTER install_registry_ca so that --config takes precedence when | ||
| 742 | # both mechanisms are used. | ||
| 743 | install_auth_config | ||
| 744 | |||
| 687 | # Start containerd and dockerd (Docker-specific) | 745 | # Start containerd and dockerd (Docker-specific) |
| 688 | start_containerd | 746 | start_containerd |
| 689 | start_dockerd | 747 | start_dockerd |
diff --git a/recipes-containers/vcontainer/files/vpdmn-init.sh b/recipes-containers/vcontainer/files/vpdmn-init.sh index 7f661102..2036ed39 100755 --- a/recipes-containers/vcontainer/files/vpdmn-init.sh +++ b/recipes-containers/vcontainer/files/vpdmn-init.sh | |||
| @@ -20,6 +20,12 @@ | |||
| 20 | # podman_output=<type> Output type: text, tar, storage (default: text) | 20 | # podman_output=<type> Output type: text, tar, storage (default: text) |
| 21 | # podman_state=<type> State type: none, disk (default: none) | 21 | # podman_state=<type> State type: none, disk (default: none) |
| 22 | # podman_network=1 Enable networking (configure eth0, DNS) | 22 | # podman_network=1 Enable networking (configure eth0, DNS) |
| 23 | # podman_auth=1 A pre-built registry auth file (docker config.json | ||
| 24 | # schema, "auths" block) is available on a dedicated | ||
| 25 | # read-only 9p share tagged "vpdmn_auth" (mounted at | ||
| 26 | # /mnt/auth). Installed as /run/containers/0/auth.json | ||
| 27 | # (the rootful podman default), and exported via | ||
| 28 | # $REGISTRY_AUTH_FILE. | ||
| 23 | # | 29 | # |
| 24 | # Version: 1.1.0 | 30 | # Version: 1.1.0 |
| 25 | # | 31 | # |
| @@ -97,6 +103,57 @@ verify_podman() { | |||
| 97 | fi | 103 | fi |
| 98 | } | 104 | } |
| 99 | 105 | ||
| 106 | # Install a user-supplied registry auth file from the dedicated read-only | ||
| 107 | # auth 9p share (mounted at /mnt/auth by mount_auth_share). Podman accepts | ||
| 108 | # the same "auths" JSON schema as docker config.json, so we can copy directly. | ||
| 109 | # | ||
| 110 | # Canonical rootful path is /run/containers/0/auth.json; we also export | ||
| 111 | # $REGISTRY_AUTH_FILE so it works regardless of podman's search order. | ||
| 112 | # | ||
| 113 | # Security posture matches vdkr-init.sh install_auth_config: | ||
| 114 | # * Source is a separate read-only 9p tag ("vpdmn_auth") so it cannot leak | ||
| 115 | # into /mnt/share outputs. | ||
| 116 | # * Target has mode 0600; containing dir has mode 0700. | ||
| 117 | # * /mnt/auth is unmounted immediately after copy so user workloads in the | ||
| 118 | # VM have no open reference to the host-side staging directory. | ||
| 119 | install_auth_config() { | ||
| 120 | if [ "$RUNTIME_AUTH" != "1" ]; then | ||
| 121 | return 0 | ||
| 122 | fi | ||
| 123 | |||
| 124 | if ! mount_auth_share; then | ||
| 125 | log "WARNING: podman_auth=1 was set but the auth 9p share did not mount" | ||
| 126 | return 1 | ||
| 127 | fi | ||
| 128 | |||
| 129 | local src="$AUTH_SHARE_MOUNT/config.json" | ||
| 130 | if [ ! -f "$src" ]; then | ||
| 131 | log "WARNING: expected $src on auth share but file is missing" | ||
| 132 | unmount_auth_share | ||
| 133 | return 1 | ||
| 134 | fi | ||
| 135 | |||
| 136 | # Rootful podman's default auth path | ||
| 137 | local auth_dir="/run/containers/0" | ||
| 138 | local auth_file="$auth_dir/auth.json" | ||
| 139 | |||
| 140 | mkdir -p "$auth_dir" | ||
| 141 | chmod 700 "$auth_dir" | ||
| 142 | |||
| 143 | if cp "$src" "$auth_file" 2>/dev/null; then | ||
| 144 | chmod 600 "$auth_file" | ||
| 145 | export REGISTRY_AUTH_FILE="$auth_file" | ||
| 146 | log "Installed registry auth config at $auth_file" | ||
| 147 | else | ||
| 148 | log "ERROR: failed to copy auth config to $auth_file" | ||
| 149 | unmount_auth_share | ||
| 150 | return 1 | ||
| 151 | fi | ||
| 152 | |||
| 153 | unmount_auth_share | ||
| 154 | return 0 | ||
| 155 | } | ||
| 156 | |||
| 100 | # Podman is daemonless - nothing to stop | 157 | # Podman is daemonless - nothing to stop |
| 101 | stop_runtime_daemons() { | 158 | stop_runtime_daemons() { |
| 102 | : | 159 | : |
| @@ -190,6 +247,10 @@ configure_networking | |||
| 190 | # Verify podman is available (no daemon to start) | 247 | # Verify podman is available (no daemon to start) |
| 191 | verify_podman | 248 | verify_podman |
| 192 | 249 | ||
| 250 | # Install user-supplied auth config from the dedicated auth 9p share, if any. | ||
| 251 | # Done before command execution so pulls/logins have credentials available. | ||
| 252 | install_auth_config | ||
| 253 | |||
| 193 | # Handle daemon mode or single command execution | 254 | # Handle daemon mode or single command execution |
| 194 | if [ "$RUNTIME_DAEMON" = "1" ]; then | 255 | if [ "$RUNTIME_DAEMON" = "1" ]; then |
| 195 | run_daemon_mode | 256 | run_daemon_mode |
diff --git a/recipes-containers/vcontainer/files/vrunner-backend-qemu.sh b/recipes-containers/vcontainer/files/vrunner-backend-qemu.sh index 87054876..3ea73cc7 100644 --- a/recipes-containers/vcontainer/files/vrunner-backend-qemu.sh +++ b/recipes-containers/vcontainer/files/vrunner-backend-qemu.sh | |||
| @@ -188,10 +188,16 @@ hv_start_vm_background() { | |||
| 188 | local log_file="$2" | 188 | local log_file="$2" |
| 189 | local timeout_val="$3" | 189 | local timeout_val="$3" |
| 190 | 190 | ||
| 191 | # Fully detach stdio from the invoking shell. In daemon mode this | ||
| 192 | # process outlives vrunner.sh, and if the CLI that invoked us was | ||
| 193 | # wrapped by something that pipes stdout/stderr (e.g. a test harness | ||
| 194 | # using subprocess.run(capture_output=True)), any inherited fd here | ||
| 195 | # would block the parent's read/communicate() call until QEMU exits. | ||
| 196 | # Redirect fd 0 from /dev/null and fd 1/fd 2 to the log file. | ||
| 191 | if [ -n "$timeout_val" ]; then | 197 | if [ -n "$timeout_val" ]; then |
| 192 | timeout $timeout_val $HV_CMD $HV_OPTS -append "$kernel_append" > "$log_file" 2>&1 & | 198 | timeout $timeout_val $HV_CMD $HV_OPTS -append "$kernel_append" </dev/null > "$log_file" 2>&1 & |
| 193 | else | 199 | else |
| 194 | $HV_CMD $HV_OPTS -append "$kernel_append" > "$log_file" 2>&1 & | 200 | $HV_CMD $HV_OPTS -append "$kernel_append" </dev/null > "$log_file" 2>&1 & |
| 195 | fi | 201 | fi |
| 196 | HV_VM_PID=$! | 202 | HV_VM_PID=$! |
| 197 | } | 203 | } |
diff --git a/recipes-containers/vcontainer/files/vrunner-backend-xen.sh b/recipes-containers/vcontainer/files/vrunner-backend-xen.sh index 55e87bd1..f490f91c 100644 --- a/recipes-containers/vcontainer/files/vrunner-backend-xen.sh +++ b/recipes-containers/vcontainer/files/vrunner-backend-xen.sh | |||
| @@ -397,13 +397,23 @@ hv_start_vm_background() { | |||
| 397 | # Monitor process: stays alive while domain exists. | 397 | # Monitor process: stays alive while domain exists. |
| 398 | # vcontainer-common.sh checks /proc/$pid → alive means daemon running. | 398 | # vcontainer-common.sh checks /proc/$pid → alive means daemon running. |
| 399 | # When domain dies (xl destroy, guest reboot), monitor exits. | 399 | # When domain dies (xl destroy, guest reboot), monitor exits. |
| 400 | # | ||
| 401 | # Detach the monitor's stdio from the invoking shell: in daemon | ||
| 402 | # mode this process outlives vrunner.sh, and any inherited fd | ||
| 403 | # would keep a pipe-wrapped caller (e.g. subprocess.run with | ||
| 404 | # capture_output=True) blocked in communicate() until the domain | ||
| 405 | # exits. Redirect fd 0/1/2 and disown. | ||
| 400 | local _domname="$HV_DOMNAME" | 406 | local _domname="$HV_DOMNAME" |
| 401 | (while xl list "$_domname" >/dev/null 2>&1; do sleep 10; done) & | 407 | (while xl list "$_domname" >/dev/null 2>&1; do sleep 10; done) \ |
| 408 | </dev/null >/dev/null 2>&1 & | ||
| 402 | HV_VM_PID=$! | 409 | HV_VM_PID=$! |
| 410 | disown $! 2>/dev/null || true | ||
| 403 | else | 411 | else |
| 404 | # Ephemeral mode: capture guest console (hvc0) to log file | 412 | # Ephemeral mode: capture guest console (hvc0) to log file |
| 405 | # so the monitoring loop in vrunner.sh can see output markers | 413 | # so the monitoring loop in vrunner.sh can see output markers. |
| 406 | stdbuf -oL xl console "$HV_DOMNAME" >> "$log_file" 2>&1 & | 414 | # Detach stdin so the background reader doesn't hold the caller's |
| 415 | # fd 0 open. | ||
| 416 | stdbuf -oL xl console "$HV_DOMNAME" </dev/null >> "$log_file" 2>&1 & | ||
| 407 | _XEN_CONSOLE_PID=$! | 417 | _XEN_CONSOLE_PID=$! |
| 408 | log "DEBUG" "Console capture started (PID: $_XEN_CONSOLE_PID)" | 418 | log "DEBUG" "Console capture started (PID: $_XEN_CONSOLE_PID)" |
| 409 | fi | 419 | fi |
diff --git a/recipes-containers/vcontainer/files/vrunner.sh b/recipes-containers/vcontainer/files/vrunner.sh index b6455330..4058fe54 100755 --- a/recipes-containers/vcontainer/files/vrunner.sh +++ b/recipes-containers/vcontainer/files/vrunner.sh | |||
| @@ -38,6 +38,13 @@ TARGET_ARCH="${VDKR_ARCH:-${VPDMN_ARCH:-aarch64}}" | |||
| 38 | TIMEOUT="${VDKR_TIMEOUT:-${VPDMN_TIMEOUT:-300}}" | 38 | TIMEOUT="${VDKR_TIMEOUT:-${VPDMN_TIMEOUT:-300}}" |
| 39 | VERBOSE="${VDKR_VERBOSE:-${VPDMN_VERBOSE:-false}}" | 39 | VERBOSE="${VDKR_VERBOSE:-${VPDMN_VERBOSE:-false}}" |
| 40 | 40 | ||
| 41 | # Registry authentication config file (docker config.json / podman auth.json). | ||
| 42 | # Can be set via $VDKR_CONFIG or $VPDMN_CONFIG in the environment, and is | ||
| 43 | # overridden by the --config CLI flag below. The file is passed into the guest | ||
| 44 | # over a dedicated read-only virtio-9p share and installed into the guest | ||
| 45 | # container runtime's credential location by the init script. | ||
| 46 | AUTH_CONFIG="${VDKR_CONFIG:-${VPDMN_CONFIG:-}}" | ||
| 47 | |||
| 41 | # Runtime-specific settings (set after parsing --runtime) | 48 | # Runtime-specific settings (set after parsing --runtime) |
| 42 | set_runtime_config() { | 49 | set_runtime_config() { |
| 43 | case "$RUNTIME" in | 50 | case "$RUNTIME" in |
| @@ -232,6 +239,12 @@ OPTIONS: | |||
| 232 | --network, -n Enable networking (slirp user-mode, outbound only) | 239 | --network, -n Enable networking (slirp user-mode, outbound only) |
| 233 | --registry <url> Default registry for unqualified images (e.g., 10.0.2.2:5000/yocto) | 240 | --registry <url> Default registry for unqualified images (e.g., 10.0.2.2:5000/yocto) |
| 234 | --insecure-registry <host:port> Mark registry as insecure (HTTP). Can repeat. | 241 | --insecure-registry <host:port> Mark registry as insecure (HTTP). Can repeat. |
| 242 | --config <path> Path to docker/podman auth config (config.json / auth.json). | ||
| 243 | Defaults to $VDKR_CONFIG or $VPDMN_CONFIG from environment. | ||
| 244 | The file is passed to the guest over a dedicated read-only | ||
| 245 | virtio-9p share and installed at /root/.docker/config.json | ||
| 246 | (vdkr) or /run/containers/0/auth.json (vpdmn). The host file | ||
| 247 | must be a regular file with mode 0600 or stricter. | ||
| 235 | --interactive, -it Run in interactive mode (connects terminal to container) | 248 | --interactive, -it Run in interactive mode (connects terminal to container) |
| 236 | --timeout <secs> QEMU timeout [default: 300] | 249 | --timeout <secs> QEMU timeout [default: 300] |
| 237 | --idle-timeout <s> Daemon idle timeout in seconds [default: 1800] | 250 | --idle-timeout <s> Daemon idle timeout in seconds [default: 1800] |
| @@ -406,6 +419,13 @@ while [ $# -gt 0 ]; do | |||
| 406 | REGISTRY_PASS="$2" | 419 | REGISTRY_PASS="$2" |
| 407 | shift 2 | 420 | shift 2 |
| 408 | ;; | 421 | ;; |
| 422 | --config) | ||
| 423 | # Path to a docker/podman config file (config.json / auth.json) | ||
| 424 | # Overrides $VDKR_CONFIG / $VPDMN_CONFIG. The file is mounted into | ||
| 425 | # the guest via a dedicated read-only virtio-9p share. | ||
| 426 | AUTH_CONFIG="$2" | ||
| 427 | shift 2 | ||
| 428 | ;; | ||
| 409 | --interactive|-it) | 429 | --interactive|-it) |
| 410 | INTERACTIVE="true" | 430 | INTERACTIVE="true" |
| 411 | shift | 431 | shift |
| @@ -587,13 +607,63 @@ daemon_stop() { | |||
| 587 | local pid=$(cat "$DAEMON_PID_FILE") | 607 | local pid=$(cat "$DAEMON_PID_FILE") |
| 588 | log "INFO" "Stopping daemon (PID: $pid)..." | 608 | log "INFO" "Stopping daemon (PID: $pid)..." |
| 589 | 609 | ||
| 590 | # Send shutdown command via socket | 610 | # Send shutdown command via socket, then poll until the VM exits. |
| 611 | # | ||
| 612 | # The guest's graceful_shutdown() does sync + umount of | ||
| 613 | # /var/lib/containers/storage + blockdev --flushbufs + sync + sleep 2 | ||
| 614 | # + reboot -f. Under load (e.g. tens of MB of just-imported layer | ||
| 615 | # blobs awaiting ext4 journal commit) this routinely takes 5-30 | ||
| 616 | # seconds. A fixed 2-second wait followed by SIGTERM kills the | ||
| 617 | # guest mid-umount and leaves the state disk's ext4 journal | ||
| 618 | # half-committed: layer files have correct inode metadata but | ||
| 619 | # partially-unwritten data extents, and the next session's reads | ||
| 620 | # hit EOF or CRC failures during tar-split layer reassembly: | ||
| 621 | # | ||
| 622 | # Error: reading blob sha256:<hash>: EOF | ||
| 623 | # Error: reading blob sha256:<hash>: file integrity checksum | ||
| 624 | # failed for "<file>" | ||
| 591 | if [ -S "$DAEMON_SOCKET" ]; then | 625 | if [ -S "$DAEMON_SOCKET" ]; then |
| 592 | echo "===SHUTDOWN===" | socat - "UNIX-CONNECT:$DAEMON_SOCKET" 2>/dev/null || true | 626 | echo "===SHUTDOWN===" | socat - "UNIX-CONNECT:$DAEMON_SOCKET" 2>/dev/null || true |
| 593 | sleep 2 | 627 | # Poll up to 60s (120 * 0.5s). Generous enough to cover heavy |
| 628 | # ext4 journal commits; short enough that a truly hung guest | ||
| 629 | # doesn't block the caller indefinitely. | ||
| 630 | for _i in $(seq 1 120); do | ||
| 631 | kill -0 "$pid" 2>/dev/null || break | ||
| 632 | sleep 0.5 | ||
| 633 | done | ||
| 594 | fi | 634 | fi |
| 595 | 635 | ||
| 596 | # If still running, kill it | 636 | # If still running after the graceful window, the guest didn't complete |
| 637 | # its graceful_shutdown() — meaning the state disk's ext4 journal may | ||
| 638 | # not have committed all pending writes from this session. Any escalation | ||
| 639 | # from here on risks leaving the disk image partially-written: layer | ||
| 640 | # files with correct inode metadata but unwritten data extents, which | ||
| 641 | # surface as "reading blob ...: EOF" or "file integrity checksum failed" | ||
| 642 | # errors on the *next* session's reads. Warn loudly so the operator can | ||
| 643 | # decide whether to start the next session with `memres restart --clean`. | ||
| 644 | if kill -0 "$pid" 2>/dev/null; then | ||
| 645 | log "WARN" "Guest did not exit within graceful window — state disk integrity may be compromised." | ||
| 646 | log "WARN" "If subsequent sessions report 'reading blob ...: EOF' or 'file integrity checksum failed'," | ||
| 647 | log "WARN" "discard state with: ${VCONTAINER_RUNTIME_NAME:-vrunner} memres restart --clean" | ||
| 648 | fi | ||
| 649 | |||
| 650 | # If still running after the graceful window, escalate via QMP quit. | ||
| 651 | # Functionally similar to SIGTERM at the QEMU-process level (both | ||
| 652 | # converge on qemu_system_killed and a block-layer flush), but goes | ||
| 653 | # through QEMU's monitor interface — the same path hv_idle_shutdown() | ||
| 654 | # uses. Keeps the two escalation paths consistent. | ||
| 655 | if kill -0 "$pid" 2>/dev/null; then | ||
| 656 | local qmp_sock="$DAEMON_SOCKET_DIR/qmp.sock" | ||
| 657 | if [ -S "$qmp_sock" ]; then | ||
| 658 | log "INFO" "Sending QMP quit..." | ||
| 659 | echo '{"execute":"qmp_capabilities"}{"execute":"quit"}' | \ | ||
| 660 | socat - "UNIX-CONNECT:$qmp_sock" >/dev/null 2>&1 || true | ||
| 661 | sleep 2 | ||
| 662 | fi | ||
| 663 | fi | ||
| 664 | |||
| 665 | # If QMP quit didn't take (or no QMP socket — older configs), fall | ||
| 666 | # back to SIGTERM. | ||
| 597 | if kill -0 "$pid" 2>/dev/null; then | 667 | if kill -0 "$pid" 2>/dev/null; then |
| 598 | log "INFO" "Sending SIGTERM..." | 668 | log "INFO" "Sending SIGTERM..." |
| 599 | kill "$pid" 2>/dev/null || true | 669 | kill "$pid" 2>/dev/null || true |
| @@ -847,6 +917,124 @@ fi | |||
| 847 | TEMP_DIR="${TMPDIR:-/tmp}/vdkr-$$" | 917 | TEMP_DIR="${TMPDIR:-/tmp}/vdkr-$$" |
| 848 | mkdir -p "$TEMP_DIR" | 918 | mkdir -p "$TEMP_DIR" |
| 849 | 919 | ||
| 920 | # ============================================================================ | ||
| 921 | # Registry auth config (docker config.json / podman auth.json) | ||
| 922 | # ============================================================================ | ||
| 923 | # The AUTH_CONFIG path (from $VDKR_CONFIG, $VPDMN_CONFIG, or --config) points | ||
| 924 | # to a file containing container-registry credentials. For defence-in-depth we: | ||
| 925 | # * reject non-regular files (symlinks, devices, directories) | ||
| 926 | # * reject files readable by group/other (mode must be <= 0600) | ||
| 927 | # * warn if the file is not owned by the invoking user | ||
| 928 | # * copy it into a private per-invocation directory under $TEMP_DIR at 0400 | ||
| 929 | # * expose it to the guest via a *separate* read-only virtio-9p tag | ||
| 930 | # ("${TOOL_NAME}_auth") mounted at /mnt/auth (not the generic /mnt/share | ||
| 931 | # which holds input/output and is wiped between daemon commands) | ||
| 932 | # * never pass the file contents or path on the kernel cmdline; only a flag | ||
| 933 | # "${CMDLINE_PREFIX}_auth=1" to tell the init script to look at /mnt/auth | ||
| 934 | # * rely on the existing $TEMP_DIR EXIT/INT/TERM trap to delete the copy | ||
| 935 | # | ||
| 936 | # The auth file is never logged (path is visible, but contents are not). | ||
| 937 | AUTH_SHARE_DIR="" | ||
| 938 | |||
| 939 | validate_auth_config() { | ||
| 940 | local path="$1" | ||
| 941 | |||
| 942 | # Resolve symlinks to the canonical path so the perm check applies to the | ||
| 943 | # actual file, but still require the *named* path to be a regular file | ||
| 944 | # (not a symlink pointing into sensitive areas like /proc/self/environ). | ||
| 945 | if [ -L "$path" ]; then | ||
| 946 | log "ERROR" "--config must not be a symlink: $path" | ||
| 947 | return 1 | ||
| 948 | fi | ||
| 949 | if [ ! -e "$path" ]; then | ||
| 950 | log "ERROR" "--config file not found: $path" | ||
| 951 | return 1 | ||
| 952 | fi | ||
| 953 | if [ ! -f "$path" ]; then | ||
| 954 | log "ERROR" "--config must be a regular file: $path" | ||
| 955 | return 1 | ||
| 956 | fi | ||
| 957 | if [ ! -r "$path" ]; then | ||
| 958 | log "ERROR" "--config file is not readable: $path" | ||
| 959 | return 1 | ||
| 960 | fi | ||
| 961 | |||
| 962 | # Size sanity: docker config.json / podman auth.json should be small. | ||
| 963 | # 1 MiB is already generous. Reject unusually large files to avoid | ||
| 964 | # accidentally shipping a large credential blob. | ||
| 965 | local size | ||
| 966 | size=$(stat -c %s "$path" 2>/dev/null || echo 0) | ||
| 967 | if [ "$size" -gt 1048576 ]; then | ||
| 968 | log "ERROR" "--config file is too large ($size bytes, max 1 MiB): $path" | ||
| 969 | return 1 | ||
| 970 | fi | ||
| 971 | # Minimum valid JSON object "{}" is 2 bytes. Anything smaller (including a | ||
| 972 | # 0-byte truncation or a lone newline from "echo '' > file") can't be a | ||
| 973 | # real auth config; reject rather than silently shipping garbage. | ||
| 974 | if [ "$size" -lt 2 ]; then | ||
| 975 | log "ERROR" "--config file is empty or too small to be valid JSON: $path" | ||
| 976 | return 1 | ||
| 977 | fi | ||
| 978 | |||
| 979 | # Permission check: must not be readable by group or world. | ||
| 980 | local mode | ||
| 981 | mode=$(stat -c %a "$path" 2>/dev/null || echo 0) | ||
| 982 | # stat %a emits octal without leading zero. Forbid any group/other bits. | ||
| 983 | case "$mode" in | ||
| 984 | 400|600|200) ;; | ||
| 985 | *) | ||
| 986 | log "ERROR" "--config file has unsafe permissions ($mode); expected 0600 or 0400." | ||
| 987 | log "ERROR" "Fix with: chmod 600 \"$path\"" | ||
| 988 | return 1 | ||
| 989 | ;; | ||
| 990 | esac | ||
| 991 | |||
| 992 | # Ownership check: warn if file is not owned by the current user. | ||
| 993 | local uid owner | ||
| 994 | uid=$(id -u) | ||
| 995 | owner=$(stat -c %u "$path" 2>/dev/null || echo "") | ||
| 996 | if [ -n "$owner" ] && [ "$owner" != "$uid" ]; then | ||
| 997 | log "WARN" "--config file is not owned by current user (uid=$uid, owner=$owner)" | ||
| 998 | fi | ||
| 999 | |||
| 1000 | return 0 | ||
| 1001 | } | ||
| 1002 | |||
| 1003 | # Stage the auth config into a dedicated read-only 9p share. Must be called | ||
| 1004 | # AFTER $TEMP_DIR exists and AFTER hypervisor backend functions are sourced. | ||
| 1005 | # Sets $AUTH_SHARE_DIR and appends to $HV_OPTS / $KERNEL_APPEND. | ||
| 1006 | setup_auth_share() { | ||
| 1007 | [ -z "$AUTH_CONFIG" ] && return 0 | ||
| 1008 | |||
| 1009 | if ! validate_auth_config "$AUTH_CONFIG"; then | ||
| 1010 | log "ERROR" "Refusing to stage $AUTH_CONFIG — see above." | ||
| 1011 | exit 1 | ||
| 1012 | fi | ||
| 1013 | |||
| 1014 | AUTH_SHARE_DIR="$TEMP_DIR/auth_share" | ||
| 1015 | # 0700 so nothing outside our process can peek at the staged file. | ||
| 1016 | mkdir -p "$AUTH_SHARE_DIR" | ||
| 1017 | chmod 700 "$AUTH_SHARE_DIR" | ||
| 1018 | |||
| 1019 | # Always stage as config.json regardless of source filename — the guest | ||
| 1020 | # init script knows to look for this fixed name. | ||
| 1021 | if ! cp "$AUTH_CONFIG" "$AUTH_SHARE_DIR/config.json"; then | ||
| 1022 | log "ERROR" "Failed to stage auth config" | ||
| 1023 | exit 1 | ||
| 1024 | fi | ||
| 1025 | chmod 400 "$AUTH_SHARE_DIR/config.json" | ||
| 1026 | |||
| 1027 | local auth_tag="${TOOL_NAME}_auth" | ||
| 1028 | hv_build_9p_opts "$AUTH_SHARE_DIR" "$auth_tag" "readonly=on" | ||
| 1029 | KERNEL_APPEND="$KERNEL_APPEND ${CMDLINE_PREFIX}_auth=1" | ||
| 1030 | |||
| 1031 | # Deliberately log the *fact* of staging, not the path contents or | ||
| 1032 | # credentials. The path itself is useful for debugging and appears in | ||
| 1033 | # --verbose mode only. | ||
| 1034 | log "INFO" "Registry auth config staged on read-only 9p share (tag=$auth_tag)" | ||
| 1035 | log "DEBUG" "Auth source: $AUTH_CONFIG" | ||
| 1036 | } | ||
| 1037 | |||
| 850 | cleanup() { | 1038 | cleanup() { |
| 851 | if [ "$KEEP_TEMP" = "true" ]; then | 1039 | if [ "$KEEP_TEMP" = "true" ]; then |
| 852 | log "DEBUG" "Keeping temp directory: $TEMP_DIR" | 1040 | log "DEBUG" "Keeping temp directory: $TEMP_DIR" |
| @@ -956,11 +1144,15 @@ if [ "$BATCH_IMPORT" = "true" ]; then | |||
| 956 | fi | 1144 | fi |
| 957 | done | 1145 | done |
| 958 | 1146 | ||
| 959 | # Add final images command to show what was imported | 1147 | # Show what was imported (informational only). |
| 1148 | # IMPORTANT: Must not use 'exit' — the command runs inside PID 1 init's | ||
| 1149 | # eval, and exit kills init → kernel panic. The import chain runs in a | ||
| 1150 | # subshell so its exit code is captured without risk. The images listing | ||
| 1151 | # is best-effort and doesn't affect the result. | ||
| 960 | if [ "$RUNTIME" = "docker" ]; then | 1152 | if [ "$RUNTIME" = "docker" ]; then |
| 961 | COMPOUND_CMD="$COMPOUND_CMD && docker images" | 1153 | COMPOUND_CMD="( $COMPOUND_CMD ); docker images 2>/dev/null; true" |
| 962 | else | 1154 | else |
| 963 | COMPOUND_CMD="$COMPOUND_CMD && podman images" | 1155 | COMPOUND_CMD="( $COMPOUND_CMD ); podman images 2>/dev/null; true" |
| 964 | fi | 1156 | fi |
| 965 | 1157 | ||
| 966 | log "DEBUG" "Batch command: $COMPOUND_CMD" | 1158 | log "DEBUG" "Batch command: $COMPOUND_CMD" |
| @@ -1306,6 +1498,10 @@ if [ "$DAEMON_MODE" = "start" ]; then | |||
| 1306 | log "DEBUG" "CA certificate copied to shared folder" | 1498 | log "DEBUG" "CA certificate copied to shared folder" |
| 1307 | fi | 1499 | fi |
| 1308 | 1500 | ||
| 1501 | # Stage registry auth config (config.json / auth.json) on a dedicated | ||
| 1502 | # read-only 9p share. See setup_auth_share() for the security model. | ||
| 1503 | setup_auth_share | ||
| 1504 | |||
| 1309 | log "INFO" "Starting daemon..." | 1505 | log "INFO" "Starting daemon..." |
| 1310 | log "DEBUG" "PID file: $DAEMON_PID_FILE" | 1506 | log "DEBUG" "PID file: $DAEMON_PID_FILE" |
| 1311 | log "DEBUG" "Socket: $DAEMON_SOCKET" | 1507 | log "DEBUG" "Socket: $DAEMON_SOCKET" |
| @@ -1361,7 +1557,18 @@ if [ "$DAEMON_MODE" = "start" ]; then | |||
| 1361 | # Set up port forwards via backend (e.g., iptables for Xen) | 1557 | # Set up port forwards via backend (e.g., iptables for Xen) |
| 1362 | hv_setup_port_forwards | 1558 | hv_setup_port_forwards |
| 1363 | 1559 | ||
| 1364 | # Start host-side idle watchdog if timeout is set | 1560 | # Start host-side idle watchdog if timeout is set. |
| 1561 | # | ||
| 1562 | # The watchdog is a long-running background subshell that outlives | ||
| 1563 | # vrunner.sh itself. It MUST fully detach from the invoking shell's | ||
| 1564 | # stdio: when the caller (e.g. the vdkr CLI, or a test harness that | ||
| 1565 | # wraps vdkr in subprocess.run(capture_output=True)) reads | ||
| 1566 | # stdout/stderr via pipes, any inherited write-end fd in the | ||
| 1567 | # watchdog keeps those pipes open and blocks the caller's | ||
| 1568 | # communicate()/read until the daemon is stopped (up to | ||
| 1569 | # IDLE_TIMEOUT, default 30 minutes). Redirect all three fds so the | ||
| 1570 | # watchdog holds no descriptors from the caller, and disown it so | ||
| 1571 | # the shell's job table doesn't retain it either. | ||
| 1365 | if [ "$IDLE_TIMEOUT" -gt 0 ] 2>/dev/null; then | 1572 | if [ "$IDLE_TIMEOUT" -gt 0 ] 2>/dev/null; then |
| 1366 | ACTIVITY_FILE="$DAEMON_SOCKET_DIR/activity" | 1573 | ACTIVITY_FILE="$DAEMON_SOCKET_DIR/activity" |
| 1367 | touch "$ACTIVITY_FILE" | 1574 | touch "$ACTIVITY_FILE" |
| @@ -1395,7 +1602,8 @@ if [ "$DAEMON_MODE" = "start" ]; then | |||
| 1395 | exit 0 | 1602 | exit 0 |
| 1396 | fi | 1603 | fi |
| 1397 | done | 1604 | done |
| 1398 | ) & | 1605 | ) </dev/null >/dev/null 2>&1 & |
| 1606 | disown $! 2>/dev/null || true | ||
| 1399 | log "DEBUG" "Started host-side idle watchdog (timeout: ${IDLE_TIMEOUT}s)" | 1607 | log "DEBUG" "Started host-side idle watchdog (timeout: ${IDLE_TIMEOUT}s)" |
| 1400 | fi | 1608 | fi |
| 1401 | 1609 | ||
| @@ -1423,6 +1631,11 @@ if [ -n "$CA_CERT" ] && [ -f "$CA_CERT" ]; then | |||
| 1423 | log "DEBUG" "CA certificate available via 9p" | 1631 | log "DEBUG" "CA certificate available via 9p" |
| 1424 | fi | 1632 | fi |
| 1425 | 1633 | ||
| 1634 | # Stage registry auth config (config.json / auth.json) on a dedicated read-only | ||
| 1635 | # 9p share for non-daemon and batch-import modes. Safe to call when AUTH_CONFIG | ||
| 1636 | # is empty — it no-ops. See setup_auth_share() for the security model. | ||
| 1637 | setup_auth_share | ||
| 1638 | |||
| 1426 | log "INFO" "Starting VM ($VCONTAINER_HYPERVISOR)..." | 1639 | log "INFO" "Starting VM ($VCONTAINER_HYPERVISOR)..." |
| 1427 | 1640 | ||
| 1428 | # Interactive mode runs VM in foreground with stdio connected | 1641 | # Interactive mode runs VM in foreground with stdio connected |
diff --git a/recipes-containers/vcontainer/vcontainer-initramfs-create.inc b/recipes-containers/vcontainer/vcontainer-initramfs-create.inc index 3e22bdb1..68011abc 100644 --- a/recipes-containers/vcontainer/vcontainer-initramfs-create.inc +++ b/recipes-containers/vcontainer/vcontainer-initramfs-create.inc | |||
| @@ -44,12 +44,6 @@ python () { | |||
| 44 | d.setVarFlag('do_compile', 'nostamp', '1') | 44 | d.setVarFlag('do_compile', 'nostamp', '1') |
| 45 | d.setVarFlag('do_deploy', 'nostamp', '1') | 45 | d.setVarFlag('do_deploy', 'nostamp', '1') |
| 46 | 46 | ||
| 47 | # Conditionally set mcdepends when our multiconfig is configured | ||
| 48 | # (avoids parse errors when BBMULTICONFIG is not set, e.g. yocto-check-layer) | ||
| 49 | mc = d.getVar('VCONTAINER_MULTICONFIG') | ||
| 50 | bbmulticonfig = (d.getVar('BBMULTICONFIG') or "").split() | ||
| 51 | if mc in bbmulticonfig: | ||
| 52 | d.setVarFlag('do_compile', 'mcdepends', 'mc:%s::virtual/kernel:do_deploy' % mc) | ||
| 53 | } | 47 | } |
| 54 | 48 | ||
| 55 | # Only populate native sysroot, skip target sysroot to avoid libgcc conflicts | 49 | # Only populate native sysroot, skip target sysroot to avoid libgcc conflicts |
| @@ -58,12 +52,17 @@ INHIBIT_DEFAULT_DEPS = "1" | |||
| 58 | # Dependencies: | 52 | # Dependencies: |
| 59 | # 1. The tiny initramfs image (produces cpio.gz) | 53 | # 1. The tiny initramfs image (produces cpio.gz) |
| 60 | # 2. The multiconfig rootfs image (produces squashfs) | 54 | # 2. The multiconfig rootfs image (produces squashfs) |
| 61 | # 3. The kernel from main build | 55 | # 3. The kernel (deployed transitively via image.bbclass do_build) |
| 62 | # | 56 | # |
| 63 | # Both initramfs and rootfs images are in the same multiconfig | 57 | # Both image recipes inherit core-image/image.bbclass which has: |
| 64 | do_compile[depends] = "${VCONTAINER_RUNTIME}-tiny-initramfs-image:do_image_complete" | 58 | # do_build[depends] += "virtual/kernel:do_deploy" |
| 65 | do_compile[depends] += "${VCONTAINER_RUNTIME}-rootfs-image:do_image_complete" | 59 | # By depending on do_build (not do_image_complete), we ensure the |
| 66 | # mcdepends set conditionally in anonymous python below | 60 | # kernel is deployed to MC_DEPLOY before do_compile copies it. |
| 61 | # Using do_image_complete cut the chain short — it runs before | ||
| 62 | # do_build, so virtual/kernel:do_deploy was not guaranteed to have | ||
| 63 | # run, causing missing kernel on sstate-accelerated builds. | ||
| 64 | do_compile[depends] = "${VCONTAINER_RUNTIME}-tiny-initramfs-image:do_build" | ||
| 65 | do_compile[depends] += "${VCONTAINER_RUNTIME}-rootfs-image:do_build" | ||
| 67 | 66 | ||
| 68 | S = "${UNPACKDIR}" | 67 | S = "${UNPACKDIR}" |
| 69 | B = "${WORKDIR}/build" | 68 | B = "${WORKDIR}/build" |
| @@ -141,14 +140,18 @@ do_compile() { | |||
| 141 | # ========================================================================= | 140 | # ========================================================================= |
| 142 | # PART 3: COPY KERNEL | 141 | # PART 3: COPY KERNEL |
| 143 | # ========================================================================= | 142 | # ========================================================================= |
| 143 | # Use the multiconfig's deploy directory (same as initramfs/rootfs), | ||
| 144 | # not DEPLOY_DIR_IMAGE which may point to the main config's deploy | ||
| 145 | # directory. The kernel is built as a dependency of the rootfs image | ||
| 146 | # within the same multiconfig. | ||
| 144 | bbnote "Copying kernel image..." | 147 | bbnote "Copying kernel image..." |
| 145 | KERNEL_FILE="${DEPLOY_DIR_IMAGE}/${KERNEL_IMAGETYPE_INITRAMFS}" | 148 | KERNEL_FILE="${MC_DEPLOY}/${KERNEL_IMAGETYPE_INITRAMFS}" |
| 146 | if [ -f "${KERNEL_FILE}" ]; then | 149 | if [ -f "${KERNEL_FILE}" ]; then |
| 147 | cp "${KERNEL_FILE}" ${B}/kernel | 150 | cp "${KERNEL_FILE}" ${B}/kernel |
| 148 | KERNEL_SIZE=$(stat -c%s ${B}/kernel) | 151 | KERNEL_SIZE=$(stat -c%s ${B}/kernel) |
| 149 | bbnote "Kernel copied: ${KERNEL_SIZE} bytes ($(expr ${KERNEL_SIZE} / 1024 / 1024)MB)" | 152 | bbnote "Kernel copied: ${KERNEL_SIZE} bytes ($(expr ${KERNEL_SIZE} / 1024 / 1024)MB)" |
| 150 | else | 153 | else |
| 151 | bbwarn "Kernel not found at ${KERNEL_FILE}" | 154 | bbwarn "Kernel not found at ${KERNEL_FILE} — check that the vruntime multiconfig kernel is built" |
| 152 | fi | 155 | fi |
| 153 | } | 156 | } |
| 154 | 157 | ||
diff --git a/recipes-containers/vcontainer/vcontainer-tarball.bb b/recipes-containers/vcontainer/vcontainer-tarball.bb index ee3c1147..f943cfd0 100644 --- a/recipes-containers/vcontainer/vcontainer-tarball.bb +++ b/recipes-containers/vcontainer/vcontainer-tarball.bb | |||
| @@ -50,6 +50,15 @@ TOOLCHAIN_TARGET_TASK = "" | |||
| 50 | TARGET_ARCH = "none" | 50 | TARGET_ARCH = "none" |
| 51 | TARGET_OS = "none" | 51 | TARGET_OS = "none" |
| 52 | 52 | ||
| 53 | # Must use ${SDK_ARCH}-${SDKPKGSUFFIX} (e.g. x86_64-nativesdk) to match | ||
| 54 | # the pattern explicitly listed in SSTATE_ARCHS, which SPDX's find_jsonld() | ||
| 55 | # searches when locating the static recipe SPDX document generated by | ||
| 56 | # do_create_recipe_spdx. Using ${SDK_ARCH}_${SDK_OS} (x86_64_linux) is NOT | ||
| 57 | # in SSTATE_ARCHS and causes do_create_spdx to fatal with "Could not find a | ||
| 58 | # static SPDX document named static-vcontainer-tarball". | ||
| 59 | # See: meta/classes-global/sstate.bbclass SSTATE_ARCHS definition and | ||
| 60 | # buildtools-tarball.bb for the reference pattern. | ||
| 61 | |||
| 53 | # Host tools to include via SDK | 62 | # Host tools to include via SDK |
| 54 | # Note: nativesdk-qemu-vcontainer is a minimal QEMU without OpenGL/virgl | 63 | # Note: nativesdk-qemu-vcontainer is a minimal QEMU without OpenGL/virgl |
| 55 | # to avoid mesa -> llvm -> clang build dependency chain | 64 | # to avoid mesa -> llvm -> clang build dependency chain |
| @@ -66,7 +75,7 @@ SDK_TITLE = "vcontainer tools (vdkr/vpdmn)" | |||
| 66 | 75 | ||
| 67 | # SDK configuration (same pattern as buildtools-tarball) | 76 | # SDK configuration (same pattern as buildtools-tarball) |
| 68 | MULTIMACH_TARGET_SYS = "${SDK_ARCH}-nativesdk${SDK_VENDOR}-${SDK_OS}" | 77 | MULTIMACH_TARGET_SYS = "${SDK_ARCH}-nativesdk${SDK_VENDOR}-${SDK_OS}" |
| 69 | PACKAGE_ARCH = "${SDK_ARCH}_${SDK_OS}" | 78 | PACKAGE_ARCH = "${SDK_ARCH}-${SDKPKGSUFFIX}" |
| 70 | PACKAGE_ARCHS = "" | 79 | PACKAGE_ARCHS = "" |
| 71 | SDK_PACKAGE_ARCHS += "vcontainer-dummy-${SDKPKGSUFFIX}" | 80 | SDK_PACKAGE_ARCHS += "vcontainer-dummy-${SDKPKGSUFFIX}" |
| 72 | 81 | ||
| @@ -131,6 +140,25 @@ VCONTAINER_ARCHITECTURES ?= "x86_64 aarch64" | |||
| 131 | 140 | ||
| 132 | # Conditionally set mcdepends based on available multiconfigs | 141 | # Conditionally set mcdepends based on available multiconfigs |
| 133 | # (avoids parse errors when BBMULTICONFIG is not set, e.g. yocto-check-layer) | 142 | # (avoids parse errors when BBMULTICONFIG is not set, e.g. yocto-check-layer) |
| 143 | # | ||
| 144 | # Two layers of dependency per arch: | ||
| 145 | # | ||
| 146 | # 1. initramfs-create:do_deploy | ||
| 147 | # Task ordering — guarantees the rootfs.img / kernel / initramfs are | ||
| 148 | # staged under tmp-<mc>/deploy/images/<machine>/<tool>/<arch>/ before | ||
| 149 | # do_populate_sdk reads them. | ||
| 150 | # | ||
| 151 | # 2. rootfs-image:do_image_complete | ||
| 152 | # Defence-in-depth for sstate consistency. Without this, a rootfs | ||
| 153 | # content change (e.g. adding netavark, switching iptables -> nftables) | ||
| 154 | # would only invalidate the tarball's sstate hash if it propagates | ||
| 155 | # cleanly through rootfs-image:do_build -> initramfs-create:do_compile | ||
| 156 | # -> initramfs-create:do_deploy -> mcdepends. Any break in that chain | ||
| 157 | # (the DEPLOY_DIR-input sstate pattern is one known way to get a stale | ||
| 158 | # hit) re-introduces the netavark-stale-tarball failure mode. Listing | ||
| 159 | # the rootfs-image task directly puts its hash in our chain regardless | ||
| 160 | # of intermediate propagation, and costs nothing if the chain was | ||
| 161 | # already healthy. | ||
| 134 | python () { | 162 | python () { |
| 135 | bbmulticonfig = (d.getVar('BBMULTICONFIG') or "").split() | 163 | bbmulticonfig = (d.getVar('BBMULTICONFIG') or "").split() |
| 136 | mcdeps = [] | 164 | mcdeps = [] |
| @@ -138,6 +166,8 @@ python () { | |||
| 138 | if mc in bbmulticonfig: | 166 | if mc in bbmulticonfig: |
| 139 | mcdeps.append('mc::%s:vdkr-initramfs-create:do_deploy' % mc) | 167 | mcdeps.append('mc::%s:vdkr-initramfs-create:do_deploy' % mc) |
| 140 | mcdeps.append('mc::%s:vpdmn-initramfs-create:do_deploy' % mc) | 168 | mcdeps.append('mc::%s:vpdmn-initramfs-create:do_deploy' % mc) |
| 169 | mcdeps.append('mc::%s:vdkr-rootfs-image:do_image_complete' % mc) | ||
| 170 | mcdeps.append('mc::%s:vpdmn-rootfs-image:do_image_complete' % mc) | ||
| 141 | if mcdeps: | 171 | if mcdeps: |
| 142 | d.setVarFlag('do_populate_sdk', 'mcdepends', ' '.join(mcdeps)) | 172 | d.setVarFlag('do_populate_sdk', 'mcdepends', ' '.join(mcdeps)) |
| 143 | 173 | ||
| @@ -307,8 +337,9 @@ Quick Start: | |||
| 307 | Architectures included: ${ARCHITECTURES} | 337 | Architectures included: ${ARCHITECTURES} |
| 308 | 338 | ||
| 309 | Contents: | 339 | Contents: |
| 310 | init-env.sh - Environment setup script | 340 | init-env.sh - Environment setup script (interactive bash) |
| 311 | vdkr, vdkr-<arch> - Docker CLI wrapper | 341 | environment-setup-ci - CI environment (for yocto-autobuilder-helper) |
| 342 | vdkr, vdkr-<arch> - Docker CLI wrapper | ||
| 312 | vpdmn, vpdmn-<arch> - Podman CLI wrapper | 343 | vpdmn, vpdmn-<arch> - Podman CLI wrapper |
| 313 | vrunner.sh - Shared QEMU runner | 344 | vrunner.sh - Shared QEMU runner |
| 314 | vcontainer-common.sh - Shared CLI code | 345 | vcontainer-common.sh - Shared CLI code |
| @@ -399,6 +430,32 @@ ENVEOF | |||
| 399 | # Create init-env.sh symlink for convenience | 430 | # Create init-env.sh symlink for convenience |
| 400 | ln -sf environment-setup-${REAL_MULTIMACH_TARGET_SYS} ${SDK_OUTPUT}/${SDKPATH}/init-env.sh | 431 | ln -sf environment-setup-${REAL_MULTIMACH_TARGET_SYS} ${SDK_OUTPUT}/${SDKPATH}/init-env.sh |
| 401 | 432 | ||
| 433 | # ----------------------------------------------------------------------- | ||
| 434 | # CI/AutoBuilder environment script | ||
| 435 | # ----------------------------------------------------------------------- | ||
| 436 | # yocto-autobuilder-helper's enable_tools_tarball() parses environment | ||
| 437 | # scripts line-by-line in Python. It only honours lines starting with | ||
| 438 | # "export " at column 0, only substitutes $PATH, and treats "unset " at | ||
| 439 | # column 0 as a removal. It does NOT evaluate shell expressions like | ||
| 440 | # $(...) or variable references like $FOO. | ||
| 441 | # | ||
| 442 | # Rather than adding conditional logic to the interactive bash script, | ||
| 443 | # generate a separate flat file with baked-in absolute paths that the | ||
| 444 | # AB parser can consume directly. SDK relocation rewrites these paths | ||
| 445 | # at install time just like the primary environment-setup-* script. | ||
| 446 | ci_script=${SDK_OUTPUT}/${SDKPATH}/environment-setup-ci | ||
| 447 | cat > $ci_script <<CISCRIPT | ||
| 448 | # vcontainer CI environment — for yocto-autobuilder-helper | ||
| 449 | # Flat export lines with absolute paths; no shell logic. | ||
| 450 | # SDK relocation rewrites these paths at install time. | ||
| 451 | export VCONTAINER_DIR="${SDKPATH}" | ||
| 452 | export OECORE_NATIVE_SYSROOT="${SDKPATHNATIVE}" | ||
| 453 | export PATH="${SDKPATH}:${SDKPATHNATIVE}/usr/bin:/usr/bin:/bin:\$PATH" | ||
| 454 | # Clean up - unset to avoid confusing other Yocto tools' >> $script | ||
| 455 | unset OECORE_NATIVE_SYSROOT | ||
| 456 | CISCRIPT | ||
| 457 | chmod 755 $ci_script | ||
| 458 | |||
| 402 | # Create version file | 459 | # Create version file |
| 403 | echo "vcontainer SDK version: ${PV}" > ${SDK_OUTPUT}/${SDKPATH}/version.txt | 460 | echo "vcontainer SDK version: ${PV}" > ${SDK_OUTPUT}/${SDKPATH}/version.txt |
| 404 | echo "Built: $(date)" >> ${SDK_OUTPUT}/${SDKPATH}/version.txt | 461 | echo "Built: $(date)" >> ${SDK_OUTPUT}/${SDKPATH}/version.txt |
diff --git a/recipes-containers/vcontainer/vdkr-rootfs-image.bb b/recipes-containers/vcontainer/vdkr-rootfs-image.bb index e2921ec7..fb5a3718 100644 --- a/recipes-containers/vcontainer/vdkr-rootfs-image.bb +++ b/recipes-containers/vcontainer/vdkr-rootfs-image.bb | |||
| @@ -47,6 +47,8 @@ inherit core-image | |||
| 47 | # We need Docker and container tools | 47 | # We need Docker and container tools |
| 48 | # Note: runc is explicitly listed because vruntime distro sets | 48 | # Note: runc is explicitly listed because vruntime distro sets |
| 49 | # VIRTUAL-RUNTIME_container_runtime="" to avoid runc/crun conflicts. | 49 | # VIRTUAL-RUNTIME_container_runtime="" to avoid runc/crun conflicts. |
| 50 | # Note: skopeo is required inside the guest for batch import | ||
| 51 | # (skopeo copy oci:... containers-storage:...). | ||
| 50 | IMAGE_INSTALL = " \ | 52 | IMAGE_INSTALL = " \ |
| 51 | packagegroup-core-boot \ | 53 | packagegroup-core-boot \ |
| 52 | docker-moby \ | 54 | docker-moby \ |
| @@ -58,6 +60,7 @@ IMAGE_INSTALL = " \ | |||
| 58 | iptables \ | 60 | iptables \ |
| 59 | util-linux \ | 61 | util-linux \ |
| 60 | kernel-modules \ | 62 | kernel-modules \ |
| 63 | ca-certificates \ | ||
| 61 | " | 64 | " |
| 62 | 65 | ||
| 63 | # No extra features needed | 66 | # No extra features needed |
diff --git a/recipes-containers/vcontainer/vpdmn-rootfs-image.bb b/recipes-containers/vcontainer/vpdmn-rootfs-image.bb index 8808e6b2..909e1298 100644 --- a/recipes-containers/vcontainer/vpdmn-rootfs-image.bb +++ b/recipes-containers/vcontainer/vpdmn-rootfs-image.bb | |||
| @@ -42,6 +42,8 @@ inherit core-image | |||
| 42 | # Podman is daemonless - no containerd required! | 42 | # Podman is daemonless - no containerd required! |
| 43 | # Note: crun is explicitly listed because vruntime distro sets | 43 | # Note: crun is explicitly listed because vruntime distro sets |
| 44 | # VIRTUAL-RUNTIME_container_runtime="" to avoid runc/crun conflicts. | 44 | # VIRTUAL-RUNTIME_container_runtime="" to avoid runc/crun conflicts. |
| 45 | # Note: skopeo is required inside the guest for batch import | ||
| 46 | # (skopeo copy oci:... containers-storage:...). | ||
| 45 | IMAGE_INSTALL = " \ | 47 | IMAGE_INSTALL = " \ |
| 46 | packagegroup-core-boot \ | 48 | packagegroup-core-boot \ |
| 47 | podman \ | 49 | podman \ |
| @@ -52,7 +54,7 @@ IMAGE_INSTALL = " \ | |||
| 52 | aardvark-dns \ | 54 | aardvark-dns \ |
| 53 | busybox \ | 55 | busybox \ |
| 54 | iproute2 \ | 56 | iproute2 \ |
| 55 | iptables \ | 57 | nftables \ |
| 56 | util-linux \ | 58 | util-linux \ |
| 57 | ca-certificates \ | 59 | ca-certificates \ |
| 58 | " | 60 | " |
| @@ -121,12 +123,22 @@ EOF | |||
| 121 | 123 | ||
| 122 | # Create containers.conf for podman engine settings | 124 | # Create containers.conf for podman engine settings |
| 123 | cat > ${IMAGE_ROOTFS}/etc/containers/containers.conf << 'EOF' | 125 | cat > ${IMAGE_ROOTFS}/etc/containers/containers.conf << 'EOF' |
| 124 | [engine] | ||
| 125 | # Location of helper binaries (netavark, aardvark-dns) | ||
| 126 | helper_binaries_dir = ["/usr/libexec/podman"] | ||
| 127 | |||
| 128 | [network] | 126 | [network] |
| 129 | # Use netavark as the network backend | ||
| 130 | network_backend = "netavark" | 127 | network_backend = "netavark" |
| 131 | EOF | 128 | EOF |
| 129 | |||
| 130 | # Prevent libnss_systemd segfaults — systemd is not running in the | ||
| 131 | # vruntime VM (busybox init), but libnss_systemd.so is installed as | ||
| 132 | # a dependency. Override nsswitch.conf to use only files/compat. | ||
| 133 | cat > ${IMAGE_ROOTFS}/etc/nsswitch.conf << 'EOF' | ||
| 134 | passwd: files | ||
| 135 | group: files | ||
| 136 | shadow: files | ||
| 137 | hosts: files dns | ||
| 138 | networks: files | ||
| 139 | protocols: files | ||
| 140 | services: files | ||
| 141 | ethers: files | ||
| 142 | rpc: files | ||
| 143 | EOF | ||
| 132 | } | 144 | } |
