Monorepo for Tangled tangled.org
5

Configure Feed

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

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