Monorepo for Tangled tangled.org
5

Configure Feed

Select the types of activity you want to include in your feed.

at icy/ytnwlw 30 kB View raw
1#!/usr/bin/env bash 2set -euo pipefail 3# note: needs `sudo modprobe vhost_vsock`! 4 5log() { 6 printf "\n\033[1;36m>>> %s\033[0m\n" "$*" 7} 8 9strip_ansi() { 10 local esc 11 esc=$(printf '\033') 12 sed -E "s/${esc}\[[0-9;]*[a-zA-Z]//g; s/${esc}\([a-zA-Z]//g" "$@" 13} 14 15# check_needles OUT NEEDLES... 16check_needles() { 17 local out="$1" 18 shift 19 local clean 20 clean=$(echo "$out" | strip_ansi) 21 22 local needle missing=0 23 for needle in "$@"; do 24 if ! echo "$clean" | grep -qE "$needle"; then 25 echo "error: output missing '$needle'" >&2 26 missing=1 27 fi 28 done 29 30 if [ "$missing" -ne 0 ]; then 31 echo "$clean" >&2 32 return 1 33 fi 34} 35 36declare -a TEST_NAMES=() 37declare -a TEST_STATUSES=() 38declare -a TEST_TIMES=() 39SKIP_TEST_RC=200 40 41get_time_ms() { 42 local t="${EPOCHREALTIME:-}" 43 if [[ "$t" == *.* ]]; then 44 local secs="${t%.*}" 45 local subs="${t#*.}" 46 subs="${subs:0:3}" 47 while [ "${#subs}" -lt 3 ]; do 48 subs="${subs}0" 49 done 50 echo "${secs}${subs}" 51 else 52 echo "$(date +%s)000" 53 fi 54} 55 56format_duration() { 57 local ms=$1 58 local secs=$((ms / 1000)) 59 local rem=$((ms % 1000)) 60 printf "%d.%03ds" "$secs" "$rem" 61} 62 63print_summary() { 64 if [ "${#TEST_NAMES[@]}" -eq 0 ]; then 65 return 66 fi 67 printf "\n" 68 log "test summary" 69 echo "=========================================" 70 local passed_count=0 71 local skipped_count=0 72 local failed_count=0 73 local total_time=0 74 for i in "${!TEST_NAMES[@]}"; do 75 local name="${TEST_NAMES[$i]}" 76 local status="${TEST_STATUSES[$i]}" 77 local duration_ms="${TEST_TIMES[$i]}" 78 local duration_str 79 duration_str=$(format_duration "$duration_ms") 80 81 local status_color="\033[0;32m" 82 if [ "$status" = "Failed" ]; then 83 status_color="\033[0;31m" 84 failed_count=$((failed_count + 1)) 85 elif [ "$status" = "Skipped" ]; then 86 status_color="\033[0;33m" 87 skipped_count=$((skipped_count + 1)) 88 else 89 passed_count=$((passed_count + 1)) 90 fi 91 total_time=$((total_time + duration_ms)) 92 93 printf " %-30s %b%-8b\033[0m %s\n" "$name" "$status_color" "$status" "$duration_str" 94 done 95 echo "-----------------------------------------" 96 local total_tests="${#TEST_NAMES[@]}" 97 local total_time_str 98 total_time_str=$(format_duration "$total_time") 99 printf " total: %d tests, %d passed, %d skipped, %d failed\n" "$total_tests" "$passed_count" "$skipped_count" "$failed_count" 100 printf " total execution time: %s\n" "$total_time_str" 101 echo "=========================================" 102} 103 104skip_test() { 105 echo "skipped: $*" 106 return "$SKIP_TEST_RC" 107} 108 109host_is_nixos() { 110 [ -e /etc/NIXOS ] && return 0 111 [ -r /etc/os-release ] && grep -q '^ID=nixos$' /etc/os-release 112} 113 114JOBS="${JOBS:-4}" 115while [[ $# -gt 0 ]]; do 116 case "$1" in 117 -j | --jobs) 118 JOBS="$2" 119 shift 2 120 ;; 121 --jobs=*) 122 JOBS="${1#*=}" 123 shift 124 ;; 125 --only) 126 TEST_ONLY="$2" 127 shift 2 128 ;; 129 --only=*) 130 TEST_ONLY="${1#*=}" 131 shift 132 ;; 133 *) 134 echo "unknown argument: $1" >&2 135 echo "usage: $0 [-j N|--jobs N] [--only TEST]" >&2 136 exit 1 137 ;; 138 esac 139done 140if ! [[ "$JOBS" =~ ^[0-9]+$ ]] || [ "$JOBS" -lt 1 ]; then 141 echo "error: --jobs must be a positive integer (got '$JOBS')" >&2 142 exit 1 143fi 144 145pick_free_port() { 146 local port 147 for _ in $(seq 1 50); do 148 port=$(((RANDOM % 16384) + 20000)) 149 if ! (exec 3<>"/dev/tcp/127.0.0.1/$port") 2>/dev/null; then 150 echo "$port" 151 return 0 152 fi 153 done 154 echo "error: could not find a free port for the cache" >&2 155 return 1 156} 157 158SUCCESS=0 159rm -rf /tmp/test-spindle-microvm-logs 160 161log "setup local cache & temp environment" 162TEMP_DIR=$(mktemp -d -t test-spindle-microvm-XXXXXX) 163 164if [ ! -e /dev/vsock ]; then 165 echo "error: /dev/vsock is missing; run sudo modprobe vhost_vsock" >&2 166 exit 1 167fi 168 169log "build spindle & microvm image tarball" 170nix develop --command go build -o spindle/spindle-microvm-run ./cmd/spindle-microvm-run 171TARBALL_PATH=$(nix build .#spindle-nixos-image-tarball --no-link --print-out-paths) 172mkdir -p "$TEMP_DIR/image" 173tar -C "$TEMP_DIR/image" -xzf "$TARBALL_PATH" 174IMAGE_SPEC_JSON="$TEMP_DIR/image/spec.json" 175 176log "build alpine microvm image tarball" 177ALPINE_TARBALL_PATH=$(nix build .#spindle-alpine-image-tarball --no-link --print-out-paths) 178mkdir -p "$TEMP_DIR/alpine-image" 179tar -C "$TEMP_DIR/alpine-image" -xzf "$ALPINE_TARBALL_PATH" 180ALPINE_IMAGE_SPEC_JSON="$TEMP_DIR/alpine-image/spec.json" 181 182kill_temp_dir_procs() { 183 if [ -f "$TEMP_DIR/ncps.pid" ]; then 184 kill "$(cat "$TEMP_DIR/ncps.pid")" 2>/dev/null || true 185 fi 186 pkill -TERM -f "$TEMP_DIR" 2>/dev/null || true 187 local i 188 for i in $(seq 1 20); do 189 pgrep -f "$TEMP_DIR" >/dev/null 2>&1 || break 190 sleep 0.25 191 done 192 pkill -KILL -f "$TEMP_DIR" 2>/dev/null || true 193} 194 195collect_logs() { 196 echo "test failed. copying logs to /tmp/test-spindle-microvm-logs" 197 mkdir -p /tmp/test-spindle-microvm-logs 198 local f 199 for f in "$TEMP_DIR"/*; do 200 [ -f "$f" ] && cp "$f" /tmp/test-spindle-microvm-logs/ 201 done 202 local work logf 203 for work in "$TEMP_DIR"/work-*; do 204 [ -d "$work" ] || continue 205 for logf in "$work"/*.log; do 206 [ -f "$logf" ] || continue 207 strip_ansi "$logf" > "/tmp/test-spindle-microvm-logs/$(basename "$work")-$(basename "$logf")" 208 done 209 done 210} 211 212CLEANED=0 213cleanup() { 214 [ "$CLEANED" -eq 1 ] && return 215 CLEANED=1 216 217 print_summary 218 log "cleaning up..." 219 220 local jobs_pids 221 jobs_pids=$(jobs -p) 222 [ -n "$jobs_pids" ] && kill $jobs_pids 2>/dev/null || true 223 224 kill_temp_dir_procs 225 226 [ "$SUCCESS" -ne 1 ] && collect_logs 227 228 chmod -R +w "$TEMP_DIR" 2>/dev/null || true 229 rm -rf "$TEMP_DIR" 230 echo "done" 231} 232trap cleanup EXIT 233# route signals through the EXIT trap so an interrupt still tears down VMs. 234trap 'exit 130' INT 235trap 'exit 143' TERM 236 237CACHE_PORT=$(pick_free_port) 238./spindle/engines/microvm/start-test-cache.sh "$TEMP_DIR" "$CACHE_PORT" 239source "$TEMP_DIR/env.sh" 240 241run_vm() { 242 local name="" 243 local timeout="60s" 244 local upload=0 245 local upload_url="" 246 local activate="" 247 local no_cache=0 248 local db="" 249 local spec="$IMAGE_SPEC_JSON" 250 251 while [[ $# -gt 0 ]]; do 252 case "$1" in 253 --spec) 254 spec="$2" 255 shift 2 256 ;; 257 --name) 258 name="$2" 259 shift 2 260 ;; 261 --timeout) 262 timeout="$2" 263 shift 2 264 ;; 265 --upload) 266 upload=1 267 shift 268 ;; 269 --upload-url) 270 upload=1 271 upload_url="$2" 272 shift 2 273 ;; 274 --activate) 275 activate="$2" 276 shift 2 277 ;; 278 --no-cache) 279 no_cache=1 280 shift 281 ;; 282 --db) 283 db="$2" 284 shift 2 285 ;; 286 --) 287 shift 288 break 289 ;; 290 *) 291 echo "unknown argument: $1" >&2 292 exit 1 293 ;; 294 esac 295 done 296 297 local work_dir="$TEMP_DIR/work-${name}" 298 mkdir -p "$work_dir" 299 300 local args=( 301 --image-spec "$spec" 302 --work-dir "$work_dir" 303 --exec-timeout "$timeout" 304 --port "${SPINDLE_TEST_VSOCK_PORT:-10240}" 305 --memory-mib 2049 306 ) 307 308 if [ "$no_cache" -eq 0 ]; then 309 args+=( 310 --cache-read-url "$CACHE_URL" 311 --cache-trusted-public-key "$CACHE_PUBKEY" 312 ) 313 fi 314 315 if [ "$upload" -eq 1 ]; then 316 if [ -z "$upload_url" ]; then 317 upload_url="$CACHE_UPLOAD_URL?secret-key=$CACHE_SECRET_KEY_PATH" 318 fi 319 args+=( 320 --cache-upload-url "$upload_url" 321 ) 322 fi 323 324 if [ -n "$activate" ]; then 325 args+=( 326 --activate-config "$activate" 327 ) 328 fi 329 330 if [ -n "$db" ]; then 331 args+=( 332 --db "$db" 333 ) 334 fi 335 336 local out 337 if ! out=$(spindle/spindle-microvm-run "${args[@]}" -- "$@" 2>&1); then 338 echo "$out" | strip_ansi >&2 339 strip_ansi "$work_dir/serial.log" >&2 340 strip_ansi "$work_dir/qemu.log" >&2 341 return 1 342 fi 343 echo "$out" 344} 345 346run_test_job() { 347 local name="$1" 348 local func="$2" 349 local port="$3" 350 export SPINDLE_TEST_VSOCK_PORT="$port" 351 352 local logfile="$TEMP_DIR/test-${name}.log" 353 local start 354 start=$(get_time_ms) 355 log "[$name] start (vsock port $port)" 356 357 local status="Passed" 358 if "$func" > "$logfile" 2>&1; then 359 status="Passed" 360 else 361 local rc=$? 362 if [ "$rc" -eq "$SKIP_TEST_RC" ]; then 363 status="Skipped" 364 else 365 status="Failed" 366 fi 367 fi 368 369 local duration_ms=$(($(get_time_ms) - start)) 370 printf '%s\t%s\n' "$status" "$duration_ms" > "$TEMP_DIR/test-${name}.status" 371 372 local duration_str 373 duration_str=$(format_duration "$duration_ms") 374 if [ "$status" = "Failed" ]; then 375 printf "\n\033[0;31m>>> [%s] FAILED (%s)\033[0m\n" "$name" "$duration_str" 376 strip_ansi "$logfile" || true 377 elif [ "$status" = "Skipped" ]; then 378 printf "\n\033[0;33m>>> [%s] skipped (%s)\033[0m\n" "$name" "$duration_str" 379 strip_ansi "$logfile" || true 380 else 381 printf "\n\033[0;32m>>> [%s] passed (%s)\033[0m\n" "$name" "$duration_str" 382 fi 383} 384 385# schedules every selected test across at most $JOBS concurrent VMs (each on its 386# own vsock port), then aggregates the per-test status files into the summary 387# arrays. returns 1 if any test failed, 0 otherwise. 388run_tests() { 389 local base_port=10240 390 local idx=0 391 local running=0 392 393 for func in "${TESTS[@]}"; do 394 local name="${func#test_}" 395 name="${name//_/-}" 396 if [ -n "${TEST_ONLY:-}" ] && [ "${TEST_ONLY}" != "$name" ]; then 397 continue 398 fi 399 400 run_test_job "$name" "$func" "$((base_port + idx))" & 401 idx=$((idx + 1)) 402 running=$((running + 1)) 403 404 if [ "$running" -ge "$JOBS" ]; then 405 wait -n || true 406 running=$((running - 1)) 407 fi 408 done 409 wait 410 411 local failed=0 412 for func in "${TESTS[@]}"; do 413 local name="${func#test_}" 414 name="${name//_/-}" 415 if [ -n "${TEST_ONLY:-}" ] && [ "${TEST_ONLY}" != "$name" ]; then 416 continue 417 fi 418 419 local statusfile="$TEMP_DIR/test-${name}.status" 420 if [ ! -f "$statusfile" ]; then 421 TEST_NAMES+=("$name") 422 TEST_STATUSES+=("Failed") 423 TEST_TIMES+=(0) 424 failed=1 425 continue 426 fi 427 428 local status duration_ms 429 IFS=$'\t' read -r status duration_ms < "$statusfile" 430 TEST_NAMES+=("$name") 431 TEST_STATUSES+=("$status") 432 TEST_TIMES+=("$duration_ms") 433 if [ "$status" = "Failed" ]; then 434 failed=1 435 fi 436 done 437 438 return "$failed" 439} 440 441test_realize() { 442 local test_store_path 443 test_store_path=$(nix-build -E 'with import <nixpkgs> {}; writeText "test-file" "hello from cache"' --no-out-link) 444 nix copy --to "$CACHE_UPLOAD_URL?secret-key=$CACHE_SECRET_KEY_PATH" "$test_store_path" 445 446 local out 447 out=$(run_vm --name "realize" --timeout "60s" -- /run/current-system/sw/bin/bash -lc ' 448set -euo pipefail 449store_path=$1 450cache_url=$(sed -n "s/^extra-substituters = //p" /run/spindle/nix.conf) 451cache_url=${cache_url%% *} 452if [ -z "$cache_url" ]; then 453 echo "error: cache URL not found in /run/spindle/nix.conf" >&2 454 exit 1 455fi 456 457http_version=$(/run/current-system/sw/bin/curl --http2-prior-knowledge -fsS -o /dev/null -w "%{http_version}" "$cache_url/nix-cache-info") 458echo "http_version=$http_version" 459case "$http_version" in 460 2|2.0) ;; 461 *) 462 echo "error: cache proxy did not negotiate HTTP/2 (got $http_version)" >&2 463 exit 1 464 ;; 465esac 466 467/run/current-system/sw/bin/nix-store --realise "$store_path" >/dev/null 468' bash "$test_store_path") || return 1 469 470 check_needles "$out" "^http_version=2(\\.0)?$" || return 1 471 echo "success: store path realized from cache and cache proxy accepted cleartext HTTP/2" 472} 473 474test_build_upload() { 475 local nix_expr='with import <nixpkgs> {}; writeText "uploaded-test-file" "hello from vm upload"' 476 local out 477 out=$(run_vm --name "build-upload" --timeout "120s" --upload -- /run/current-system/sw/bin/bash -l -c "nix-build -E '$nix_expr' --no-out-link") || return 1 478 479 local built_path 480 built_path=$(echo "$out" | strip_ansi | grep -v '\.drv' | grep -o '/nix/store/[a-z0-9]*-uploaded-test-file' | head -n 1 || true) 481 if [ -z "$built_path" ]; then 482 echo "error: could not find built store path in vm output" >&2 483 return 1 484 fi 485 echo "extracted path: $built_path" 486 487 local hash 488 hash=$(basename "$built_path" | cut -d'-' -f1) 489 if ! curl -s -f "$CACHE_URL/${hash}.narinfo" > /dev/null; then 490 echo "error: built store path was not uploaded to the binary cache" >&2 491 return 1 492 fi 493 echo "success: store path uploaded to cache" 494} 495 496test_ssh_store_upload() { 497 if ! host_is_nixos; then 498 skip_test "ssh store upload smoke only runs on nixos hosts" 499 fi 500 501 run_ssh_store_upload_case() { 502 local target="$1" 503 local label="$2" 504 local name="uploaded-test-file-${label}" 505 local content="hello from vm upload via ${label}" 506 local out 507 out=$(run_vm --name "$label" --timeout "180s" --upload-url "$target" -- /run/current-system/sw/bin/bash -l -c "nix-build -E 'with import <nixpkgs> {}; writeText \"$name\" \"$content\"' --no-out-link") || return 1 508 509 local built_path 510 built_path=$(echo "$out" | strip_ansi | grep -v '\.drv' | grep -o "/nix/store/[a-z0-9]*-${name}" | head -n 1 || true) 511 if [ -z "$built_path" ]; then 512 echo "error: could not find built store path in vm output for $label" >&2 513 echo "$out" | strip_ansi >&2 514 return 1 515 fi 516 if [ ! -e "$built_path" ]; then 517 echo "error: uploaded path missing from host store for $label: $built_path" >&2 518 return 1 519 fi 520 if [ "$(cat "$built_path")" != "$content" ]; then 521 echo "error: uploaded path content mismatch for $label" >&2 522 echo "path=$built_path" >&2 523 return 1 524 fi 525 echo "success: store path uploaded to $target via spindle" 526 } 527 528 run_ssh_store_upload_case "ssh-ng://localhost" "ssh-ng-upload" 529 run_ssh_store_upload_case "ssh://localhost" "ssh-upload" 530} 531 532test_networking() { 533 local hello_path 534 hello_path=$(nix-build -E 'with import <nixpkgs> {}; hello' --no-out-link) 535 536 local out 537 out=$(run_vm --name "networking" --timeout "120s" --no-cache -- /run/current-system/sw/bin/bash -c "/run/current-system/sw/bin/curl -I --connect-timeout 1 -m 1 http://10.0.2.2:$CACHE_PORT; /run/current-system/sw/bin/nix-store --realise $hello_path") || return 1 538 539 if echo "$out" | grep -qi -E "unreachable|timeout|failed to connect|timed out" || echo "$out" | grep -q "exited with code"; then 540 echo "success: host network access blocked" 541 else 542 echo "error: guest vm accessed host network or returned unexpected output" >&2 543 echo "$out" | strip_ansi >&2 544 return 1 545 fi 546 echo "success: guest vm reached the internet and substituted hello" 547} 548 549test_substitution_and_no_upload() { 550 local hello_path 551 hello_path=$(nix-build -E 'with import <nixpkgs> {}; hello' --no-out-link) 552 553 local out 554 out=$(run_vm --name "nixpkgs-hello" --timeout "180s" --upload -- /run/current-system/sw/bin/nix-store --realise "$hello_path") || return 1 555 556 # substituted from our proxy, and nothing uploaded back to the cache. 557 check_needles "$out" "copying path.*hello" "cache uploaded: 0" || return 1 558 echo "success: hello package substituted from upstream cache and was not uploaded" 559} 560 561# a pinned registry, reused by the dependency and registry-pin tests. 562ACTIVATION_REGISTRY='"registry": { 563 "nixpkgs": "github:nixos/nixpkgs/nixos-unstable", 564 "my-nixpkgs": "nixpkgs" 565 }' 566 567test_activation_services() { 568 local config='{ 569 "services": { 570 "openssh": { 571 "enable": true, 572 "authorizedKeysFiles": ["/etc/ssh/authorized_keys"] 573 } 574 } 575 }' 576 local out 577 out=$(run_vm --name "activation-services" --timeout "300s" --activate "$config" -- /run/current-system/sw/bin/systemctl is-active sshd) || return 1 578 check_needles "$out" "^active$" || return 1 579 echo "success: openssh service active after activation" 580} 581 582test_activation_dependencies() { 583 # pkg-config + openssl as bare dependencies (resolved via the pinned nixpkgs 584 # registry); their job is to prove the activation devshell exports build env 585 # vars like PKG_CONFIG_PATH, not just PATH. hello comes in via the github 586 # flakeref and (separately) the my-nixpkgs alias. 587 local config='{ 588 '"$ACTIVATION_REGISTRY"', 589 "dependencies": [ 590 "pkg-config", 591 "openssl", 592 "github:nixos/nixpkgs#hello", 593 "my-nixpkgs#hello" 594 ] 595 }' 596 # dependencies live in the activation devshell now, not systemPackages, so a 597 # step picks them up by sourcing the materialised env (this is what the 598 # engine's RunStep does automatically; the CLI runner does not, so we do it 599 # here). 600 local out 601 out=$(run_vm --name "activation-dependencies" --timeout "600s" --activate "$config" -- /run/current-system/sw/bin/bash -l -c ' 602env_file=/run/spindle/devshell-env.sh 603[ -f "$env_file" ] && echo "env_file=present" || echo "env_file=missing" 604 605# mirror RunStep 606. "$env_file" 607export PATH="$PATH:/run/current-system/sw/bin:/nix/var/nix/profiles/default/bin" 608 609set -euo pipefail 610# bare deps: pkg-config must be on PATH, and it must locate openssl through the 611# devshell-provided PKG_CONFIG_PATH (openssls headers live in a separate `dev` 612# output, so this also proves that output got realised into the devshell). 613echo "pkgconfig=$(pkg-config --version)" 614pkg-config --exists openssl && echo "openssl_found=yes" 615echo "openssl_version=$(pkg-config --modversion openssl)" 616inc=$(pkg-config --variable=includedir openssl) 617echo "includedir=$inc" 618[ -f "$inc/openssl/ssl.h" ] && echo "dev_headers=found" 619 620# flakeref + aliased deps 621echo "hello=$(hello)" 622') || return 1 623 624 check_needles "$out" \ 625 "env_file=present" "pkgconfig=[0-9]" "openssl_found=yes" \ 626 "openssl_version=[0-9]" "dev_headers=found" "hello=Hello, world!" || return 1 627 echo "success: bare (pkg-config + openssl, dev headers found via PKG_CONFIG_PATH), flakeref, and aliased deps all resolved" 628} 629 630test_activation_registry_pin() { 631 # the nixpkgs the image itself was built from; the registry override must NOT 632 # resolve to this. deterministic (locked in the repo flake), so safe to compare. 633 local base_nixpkgs 634 base_nixpkgs=$(nix eval --raw --impure --expr '(builtins.getFlake (toString ./.)).inputs.nixpkgs.outPath') 635 636 local config='{ 637 '"$ACTIVATION_REGISTRY"' 638 }' 639 # the pinned nixpkgs must reach the system nix config: resolving it via the 640 # flakes CLI must not error "is not locked", and it must win the <nixpkgs> nixPath. 641 local out 642 out=$(run_vm --name "activation-registry-pin" --timeout "300s" --activate "$config" -- /run/current-system/sw/bin/bash -l -c ' 643set -euo pipefail 644echo "lib_version=$(nix eval --raw nixpkgs#lib.version)" 645echo "nix_path=$(nix eval --raw --impure --expr "toString <nixpkgs>")" 646') || return 1 647 648 check_needles "$out" "lib_version=[0-9]" || return 1 649 local clean guest_nixpath 650 clean=$(echo "$out" | strip_ansi) 651 guest_nixpath=$(echo "$clean" | sed -n 's/^nix_path=//p' | head -n1) 652 if [ -z "$guest_nixpath" ] || [ "$guest_nixpath" = "$base_nixpkgs" ]; then 653 echo "error: guest <nixpkgs> nixPath did not resolve to the registry override (got '$guest_nixpath', base '$base_nixpkgs')" >&2 654 return 1 655 fi 656 echo "success: pinned nixpkgs registry reached the guest nix config (flakes CLI + <nixpkgs> nixPath)" 657} 658 659test_activation_cache_substitution() { 660 # a unique path that only exists in the configured (workflow) cache; the host 661 # seeds it so the guest can prove it substitutes through the read proxy. 662 local test_store_path 663 test_store_path=$(nix-build -E 'with import <nixpkgs> {}; writeText "activation-cache-test" "hello from the workflow cache"' --no-out-link) 664 nix copy --to "$CACHE_UPLOAD_URL?secret-key=$CACHE_SECRET_KEY_PATH" "$test_store_path" 665 666 # a trivial config: this test only cares that the read proxy serves the 667 # workflow-cache path during an activated run. 668 local out 669 out=$(run_vm --name "activation-cache-substitution" --timeout "300s" --activate '{}' -- /run/current-system/sw/bin/bash -l -c ' 670set -euo pipefail 671store_path=$1 672# the unique path only exists in the configured cache, so realising it proves it 673# was substituted through the read proxy and not built or found elsewhere. 674nix-store --realise "$store_path" >/dev/null 675echo "substituted=$(cat "$store_path")" 676' bash "$test_store_path") || return 1 677 678 check_needles "$out" "substituted=hello from the workflow cache" || return 1 679 echo "success: unique path substituted from the workflow cache through the read proxy" 680} 681 682test_activation_docker() { 683 local config='{ 684 "virtualisation": { 685 "docker": { "enable": true } 686 } 687 }' 688 # docker.service is up, but the daemon socket can lag a beat behind activation; 689 # wait for it to answer, then pull+run a real image. this drives the slimmed 690 # kernel modules: overlay.ko storage plus bridge/iptables networking out of the 691 # pruned tree, with outbound DNS/network over the guest slirp link. 692 local out 693 out=$(run_vm --name "activation-docker" --timeout "600s" --activate "$config" -- /run/current-system/sw/bin/bash -l -c ' 694set -euo pipefail 695echo "docker_unit=$(systemctl is-active docker)" 696for i in $(seq 1 60); do docker info >/dev/null 2>&1 && break; sleep 1; done 697docker info >/dev/null 698echo "storage_driver=$(docker info --format "{{.Driver}}")" 699docker run --rm alpine cat /etc/alpine-release | sed "s/^/alpine_release=/" 700docker run --rm alpine echo container-ran-ok 701') || return 1 702 703 check_needles "$out" \ 704 "^docker_unit=active$" "storage_driver=overlay(2|fs)" \ 705 "alpine_release=[0-9]+\." "container-ran-ok" || return 1 706 echo "success: docker service active, pulled and ran an alpine container on the overlay storage driver" 707} 708 709test_activation_cached_realize() { 710 local config='{ 711 "services": { 712 "openssh": { 713 "enable": true, 714 "authorizedKeysFiles": ["/etc/ssh/authorized_keys"] 715 } 716 } 717 }' 718 local db_path="$TEMP_DIR/activation-cached.db" 719 720 # first run: build the config, upload its closure, and record the toplevel in 721 # the db. nothing cached yet, so this builds from scratch. 722 local out 723 out=$(run_vm --name "activation-cached-first" --timeout "600s" --activate "$config" --db "$db_path" --upload -- /run/current-system/sw/bin/systemctl is-active sshd) || return 1 724 check_needles "$out" "^active$" || return 1 725 726 # second run: same config + db, no upload. must realize the recorded toplevel 727 # from the cache instead of rebuilding, and the cached system must come up. 728 out=$(run_vm --name "activation-cached-second" --timeout "300s" --activate "$config" --db "$db_path" -- /run/current-system/sw/bin/systemctl is-active sshd) || return 1 729 730 check_needles "$out" "realizing cached NixOS config" "^active$" || return 1 731 echo "success: second run realized the cached NixOS config from the cache and sshd came up" 732} 733 734test_alpine() { 735 local hello_path 736 hello_path=$(nix-build -E 'with import <nixpkgs> {}; hello' --no-out-link) 737 738 local out 739 out=$(run_vm --spec "$ALPINE_IMAGE_SPEC_JSON" --name "alpine" --timeout "180s" --no-cache -- /bin/sh -lc ' 740set -eu 741export HOME=/workspace 742hello_path=$1 743echo "release=$(cat /etc/alpine-release)" 744echo "user=$(id -un)" 745git version 746bash -c "echo bash=\$BASH_VERSION" 747touch /workspace/write-test 748echo "workspace writable" 749git ls-remote https://tangled.org/@tangled.org/core HEAD >/dev/null 750echo "git over https ok" 751apk add make 752echo "apk ok" 753# substitute a real package from cache.nixos.org over HTTPS and run it 754nix-store --realise "$hello_path" >/dev/null 755echo "ran=$("$hello_path/bin/hello")" 756' sh "$hello_path") || return 1 757 758 check_needles "$out" \ 759 "release=" "user=spindle-workflow" "git version" "bash=" \ 760 "workspace writable" "git over https ok" "apk ok" "ran=Hello, world!" || return 1 761 echo "success: alpine guest booted, ran as workflow user, wrote workspace, cloned + installed over the network, and substituted+ran a package from cache.nixos.org over HTTPS" 762} 763 764# asserts a store path's narinfo shows up in the local cache, retrying briefly 765# since the post-build-hook enqueues uploads asynchronously. 766cache_has_path() { 767 local path="$1" 768 local hash 769 hash=$(basename "$path" | cut -d'-' -f1) 770 local i 771 for i in $(seq 1 20); do 772 if curl -s -f "$CACHE_URL/${hash}.narinfo" > /dev/null; then 773 return 0 774 fi 775 sleep 0.5 776 done 777 return 1 778} 779 780test_alpine_nix() { 781 local test_store_path 782 test_store_path=$(nix-build -E 'with import <nixpkgs> {}; writeText "alpine-nix-test" "hello from cache to alpine"' --no-out-link) 783 nix copy --to "$CACHE_UPLOAD_URL?secret-key=$CACHE_SECRET_KEY_PATH" "$test_store_path" 784 785 # exercise the full local-cache path: daemon connectivity, substitution, 786 # store-db queries, and a build via *both* the classic (nix-build) and the 787 # new flakes/nix-command (nix build) frontends. the two build derivations 788 # use distinct names so we can confirm each got uploaded back to the cache. 789 local out 790 out=$(run_vm --spec "$ALPINE_IMAGE_SPEC_JSON" --name "alpine-nix" --timeout "180s" --upload -- /bin/sh -lc ' 791set -eu 792export HOME=/workspace 793store_path=$1 794 795echo "nix_version=$(nix --version | head -n1)" 796{ nix store info >/dev/null 2>&1 || nix store ping >/dev/null 2>&1; } && echo "daemon=ok" 797 798# substitute a path from the cache and query the store db about it 799nix-store --realise "$store_path" >/dev/null 800echo "substituted=$(cat "$store_path")" 801echo "requisites=$(nix-store --query --requisites "$store_path" | wc -l | tr -d " ")" 802nix path-info --json "$store_path" >/dev/null && echo "path_info=ok" 803 804# build via the new CLI; the substituted path is declared as a real input 805# (builtins.storePath) so nix must realise it into the build sandbox first. 806# heredoc is unquoted (the outer guest script is single-quoted, so a quoted 807# delimiter would close it), hence \$ escapes what nix/the builder must expand. 808export DEP="$store_path" 809cat > /workspace/new.nix <<NIXEOF 810let dep = builtins.storePath (builtins.getEnv "DEP"); in 811derivation { 812 name = "alpine-nix-build-new"; 813 system = "x86_64-linux"; 814 builder = "/bin/sh"; 815 # the sandbox only provides the sh builtin shell (no coreutils in PATH), so 816 # stick to builtins: the build only succeeds if nix realised the declared 817 # storePath dependency into the sandbox, where [ -r ] can see it. 818 args = [ "-c" "[ -r \${dep} ] && echo via-nix-build-with-dep > \$out" ]; 819} 820NIXEOF 821new_path=$(nix build --impure --file /workspace/new.nix --no-link --print-out-paths) 822echo "new_path=$new_path" 823echo "new_content=$(tr "\n" "|" < "$new_path")" 824 825# build via the classic CLI 826cat > /workspace/old.nix <<NIXEOF 827derivation { 828 name = "alpine-nix-build-old"; 829 system = "x86_64-linux"; 830 builder = "/bin/sh"; 831 args = [ "-c" "echo built-on-alpine > \$out" ]; 832} 833NIXEOF 834old_path=$(nix-build /workspace/old.nix --no-out-link) 835echo "old_path=$old_path" 836' sh "$test_store_path") || return 1 837 838 check_needles "$out" \ 839 "daemon=ok" "substituted=hello from cache to alpine" "path_info=ok" \ 840 "requisites=[1-9][0-9]*" "new_content=via-nix-build-with-dep" || return 1 841 842 local clean new_path old_path 843 clean=$(echo "$out" | strip_ansi) 844 new_path=$(echo "$clean" | grep -o 'new_path=/nix/store/[a-z0-9]*-alpine-nix-build-new' | cut -d= -f2) 845 old_path=$(echo "$clean" | grep -o 'old_path=/nix/store/[a-z0-9]*-alpine-nix-build-old' | cut -d= -f2) 846 if [ -z "$new_path" ] || [ -z "$old_path" ]; then 847 echo "error: could not extract both built store paths from alpine guest output" >&2 848 return 1 849 fi 850 if ! cache_has_path "$new_path"; then 851 echo "error: nix-build (new CLI) output was not uploaded to the cache" >&2 852 return 1 853 fi 854 if ! cache_has_path "$old_path"; then 855 echo "error: nix-build (classic CLI) output was not uploaded to the cache" >&2 856 return 1 857 fi 858 echo "success: alpine guest substituted, queried the store db, built via both CLIs, and uploaded both outputs" 859} 860 861TESTS=( 862 test_alpine 863 test_alpine_nix 864 test_realize 865 test_build_upload 866 test_ssh_store_upload 867 test_networking 868 test_substitution_and_no_upload 869 test_activation_services 870 test_activation_dependencies 871 test_activation_registry_pin 872 test_activation_cache_substitution 873 test_activation_docker 874 test_activation_cached_realize 875) 876 877log "running ${#TESTS[@]} tests" 878if ! run_tests; then 879 exit 1 880fi 881 882SUCCESS=1 883log "passed!!"