pull swarm closures through the nix daemon: substituters in the fragment, merged root-only netrc
This commit is contained in:
parent
63ae5d6cbf
commit
ef68baa00f
5 changed files with 582 additions and 382 deletions
|
|
@ -15,9 +15,10 @@ let
|
|||
hive = "alpha";
|
||||
host = "cache.example";
|
||||
dir = "@DIR@/attic";
|
||||
url = "https://cache.example/swarm";
|
||||
confFile = "@DIR@/nix/cache.conf";
|
||||
clientNetrc = "@DIR@/nix/netrc";
|
||||
clientGroup = "@GROUP@";
|
||||
netrcFile = "@DIR@/nix/netrc";
|
||||
hostNetrc = "@DIR@/host-netrc";
|
||||
}
|
||||
);
|
||||
in
|
||||
|
|
|
|||
|
|
@ -19,11 +19,7 @@ expect() { # <description> <got> <want>
|
|||
|
||||
dir=$(mktemp -d)
|
||||
mkdir -p "$dir/bin"
|
||||
# A group other than the primary one when the runner has one, so the group the script sets
|
||||
# differs from the one a new file gets.
|
||||
group=$(id -Gn | tr " " "\n" | grep -vx "$(id -gn)" | head -1 || true)
|
||||
group=${group:-$(id -gn)}
|
||||
sed -e "s|@DIR@|$dir|g" -e "s|@GROUP@|$group|g" "$SCRIPT" >"$dir/script.sh"
|
||||
sed -e "s|@DIR@|$dir|g" "$SCRIPT" >"$dir/script.sh"
|
||||
calls=$dir/calls
|
||||
export DIR=$dir
|
||||
|
||||
|
|
@ -51,13 +47,14 @@ STUB
|
|||
chmod +x "$dir/bin/bao"
|
||||
|
||||
# `systemctl` answers for nix-daemon.service: DAEMON_STARTED is when it last
|
||||
# became active (empty: never) and every restart request is logged.
|
||||
# became active (empty: never, so it is not running) and every call is logged.
|
||||
cat >"$dir/bin/systemctl" <<STUB
|
||||
#!$BASH
|
||||
echo "systemctl \$*" >>"$dir/systemctl"
|
||||
case "\$1" in
|
||||
show) printf '%s\n' "\${DAEMON_STARTED-}" ;;
|
||||
is-failed) exit 1 ;;
|
||||
is-active) [ -n "\${DAEMON_STARTED-}" ] ;;
|
||||
try-restart) ;;
|
||||
*) echo "unexpected call: \$*" >&2; exit 98 ;;
|
||||
esac
|
||||
|
|
@ -69,7 +66,7 @@ KEYLEAF=secret_swarm_hives_alpha_attic_public-key
|
|||
TOKENLEAF=secret_swarm_hives_alpha_attic_pull-token
|
||||
|
||||
reset() {
|
||||
rm -rf "$dir/store" "$dir/attic" "$dir/nix" "$calls" "$dir/systemctl"
|
||||
rm -rf "$dir/store" "$dir/attic" "$dir/nix" "$dir/host-netrc" "$calls" "$dir/systemctl"
|
||||
mkdir -p "$dir/store"
|
||||
: >"$calls"
|
||||
: >"$dir/systemctl"
|
||||
|
|
@ -90,43 +87,41 @@ mode() { stat -c %a "$1" 2>/dev/null || echo missing; }
|
|||
ino() { stat -c %i "$1" 2>/dev/null || echo missing; }
|
||||
body() { cat "$1" 2>/dev/null || echo missing; }
|
||||
|
||||
case="first run writes the key and the netrc, each 0600, in a 0700 directory"
|
||||
case="first run writes the key (0600, in a 0700 directory) and the netrc (0600)"
|
||||
reset
|
||||
seed "swarm:S0VZ" "tok-1"
|
||||
run
|
||||
expect "exit" $? 0
|
||||
expect "key" "$(body "$dir/attic/public-key")" "swarm:S0VZ"
|
||||
expect "netrc" "$(body "$dir/attic/netrc")" "machine cache.example password tok-1"
|
||||
expect "netrc" "$(body "$dir/nix/netrc")" "machine cache.example password tok-1"
|
||||
expect "key mode" "$(mode "$dir/attic/public-key")" 600
|
||||
expect "netrc mode" "$(mode "$dir/attic/netrc")" 600
|
||||
expect "netrc mode" "$(mode "$dir/nix/netrc")" 600
|
||||
expect "dir mode" "$(mode "$dir/attic")" 700
|
||||
expect "fragment is the key and nothing else" "$(body "$dir/nix/cache.conf")" "extra-trusted-public-keys = swarm:S0VZ"
|
||||
expect "fragment names the cache, the key and the root-only netrc" "$(body "$dir/nix/cache.conf")" "extra-substituters = https://cache.example/swarm
|
||||
extra-trusted-public-keys = swarm:S0VZ
|
||||
netrc-file = $dir/nix/netrc"
|
||||
expect "fragment mode" "$(mode "$dir/nix/cache.conf")" 644
|
||||
expect "fragment dir mode" "$(mode "$dir/nix")" 755
|
||||
expect "no secret in the fragment" "$(grep -c tok-1 "$dir/nix/cache.conf")" 0
|
||||
expect "no substituter or netrc in the fragment" "$(grep -c -E "substituter|netrc" "$dir/nix/cache.conf")" 0
|
||||
expect "client netrc" "$(body "$dir/nix/netrc")" "machine cache.example password tok-1"
|
||||
expect "client netrc mode" "$(mode "$dir/nix/netrc")" 640
|
||||
expect "client netrc group" "$(stat -c %G "$dir/nix/netrc")" "$group"
|
||||
expect "control: the token is in the netrc" "$(grep -c tok-1 "$dir/nix/netrc")" 1
|
||||
expect "the netrc the fragment names is root-only" "$(mode "$dir/nix/netrc")" 600
|
||||
|
||||
case="a second run with the same facts rewrites nothing"
|
||||
k=$(ino "$dir/attic/public-key")
|
||||
n=$(ino "$dir/attic/netrc")
|
||||
cn=$(ino "$dir/nix/netrc")
|
||||
n=$(ino "$dir/nix/netrc")
|
||||
c=$(ino "$dir/nix/cache.conf")
|
||||
run
|
||||
expect "exit" $? 0
|
||||
expect "key untouched" "$(ino "$dir/attic/public-key")" "$k"
|
||||
expect "netrc untouched" "$(ino "$dir/attic/netrc")" "$n"
|
||||
expect "netrc untouched" "$(ino "$dir/nix/netrc")" "$n"
|
||||
expect "fragment untouched" "$(ino "$dir/nix/cache.conf")" "$c"
|
||||
expect "client netrc untouched" "$(ino "$dir/nix/netrc")" "$cn"
|
||||
|
||||
case="a rotated token replaces the netrc and leaves the key"
|
||||
seed "swarm:S0VZ" "tok-2"
|
||||
run
|
||||
expect "exit" $? 0
|
||||
expect "netrc" "$(body "$dir/attic/netrc")" "machine cache.example password tok-2"
|
||||
expect "netrc mode" "$(mode "$dir/attic/netrc")" 600
|
||||
expect "netrc" "$(body "$dir/nix/netrc")" "machine cache.example password tok-2"
|
||||
expect "netrc mode" "$(mode "$dir/nix/netrc")" 600
|
||||
expect "key untouched" "$(ino "$dir/attic/public-key")" "$k"
|
||||
|
||||
case="a rotated key replaces the key file"
|
||||
|
|
@ -140,12 +135,71 @@ expect "reads" "$(grep -c '^bao kv get' "$calls")" "$(grep -c '^bao kv get -fiel
|
|||
expect "login first" "$(head -1 "$calls")" "bao login -method=cert -token-only"
|
||||
expect "something was read" "$([ "$(grep -c '^bao kv get' "$calls")" -ge 2 ] && echo yes || echo no)" yes
|
||||
|
||||
# The merged netrc: the cache's entry first, then the host's other entries as
|
||||
# they are. A missing host file is no entries.
|
||||
HOSTNETRC='machine git.example login bob password hunter2
|
||||
machine other.example
|
||||
login alice
|
||||
password s3cret'
|
||||
|
||||
case="a host netrc with two other machines keeps both, after the cache's entry"
|
||||
reset
|
||||
seed "swarm:S0VZ" "tok-1"
|
||||
printf '%s\n' "$HOSTNETRC" >"$dir/host-netrc"
|
||||
run
|
||||
expect "exit" $? 0
|
||||
expect "merged" "$(body "$dir/nix/netrc")" "machine cache.example password tok-1
|
||||
$HOSTNETRC"
|
||||
expect "mode" "$(mode "$dir/nix/netrc")" 600
|
||||
expect "the host's file is not written" "$(body "$dir/host-netrc")" "$HOSTNETRC"
|
||||
|
||||
case="a host entry for the cache's own machine is replaced and the rest is kept"
|
||||
reset
|
||||
seed "swarm:S0VZ" "tok-1"
|
||||
printf '%s\n%s\n' "$HOSTNETRC" "machine cache.example login old password stale" >"$dir/host-netrc"
|
||||
run
|
||||
expect "exit" $? 0
|
||||
expect "merged" "$(body "$dir/nix/netrc")" "machine cache.example password tok-1
|
||||
$HOSTNETRC"
|
||||
expect "control: the stale entry was in the host file" "$(grep -c stale "$dir/host-netrc")" 1
|
||||
|
||||
case="a multi-line host entry for the cache's machine goes whole"
|
||||
reset
|
||||
seed "swarm:S0VZ" "tok-1"
|
||||
printf '%s\n%s\n' "machine cache.example
|
||||
login old
|
||||
password stale" "$HOSTNETRC" >"$dir/host-netrc"
|
||||
run
|
||||
expect "exit" $? 0
|
||||
expect "merged" "$(body "$dir/nix/netrc")" "machine cache.example password tok-1
|
||||
$HOSTNETRC"
|
||||
|
||||
case="an edit to the host netrc reaches the merged file on the next run"
|
||||
reset
|
||||
seed "swarm:S0VZ" "tok-1"
|
||||
printf '%s\n' "$HOSTNETRC" >"$dir/host-netrc"
|
||||
run
|
||||
printf '%s\n' "machine new.example login n password p" >>"$dir/host-netrc"
|
||||
run
|
||||
expect "exit" $? 0
|
||||
expect "new entry merged" "$(grep -c new.example "$dir/nix/netrc")" 1
|
||||
expect "old entries kept" "$(grep -c git.example "$dir/nix/netrc")" 1
|
||||
|
||||
case="a netrc left 0640 by an earlier deploy is rewritten 0600 though its content is current"
|
||||
reset
|
||||
seed "swarm:S0VZ" "tok-1"
|
||||
run
|
||||
chmod 640 "$dir/nix/netrc"
|
||||
run
|
||||
expect "exit" $? 0
|
||||
expect "mode" "$(mode "$dir/nix/netrc")" 600
|
||||
|
||||
case="a store with no facts exits cleanly and creates nothing"
|
||||
reset
|
||||
run
|
||||
expect "exit" $? 0
|
||||
expect "no key" "$(body "$dir/attic/public-key")" missing
|
||||
expect "no netrc" "$(body "$dir/attic/netrc")" missing
|
||||
expect "no netrc" "$(body "$dir/nix/netrc")" missing
|
||||
|
||||
case="one fact without the other creates nothing"
|
||||
reset
|
||||
|
|
@ -153,7 +207,7 @@ printf '%s' "swarm:S0VZ" >"$dir/store/$KEYLEAF"
|
|||
run
|
||||
expect "exit" $? 0
|
||||
expect "no key" "$(body "$dir/attic/public-key")" missing
|
||||
expect "no netrc" "$(body "$dir/attic/netrc")" missing
|
||||
expect "no netrc" "$(body "$dir/nix/netrc")" missing
|
||||
|
||||
# The failure cases start from a good state and an extra file in the directory.
|
||||
good() {
|
||||
|
|
@ -164,7 +218,7 @@ good() {
|
|||
}
|
||||
intact() {
|
||||
expect "key kept" "$(body "$dir/attic/public-key")" "swarm:S0VZ"
|
||||
expect "netrc kept" "$(body "$dir/attic/netrc")" "machine cache.example password tok-1"
|
||||
expect "netrc kept" "$(body "$dir/nix/netrc")" "machine cache.example password tok-1"
|
||||
expect "other file kept" "$(body "$dir/attic/other")" ""
|
||||
}
|
||||
|
||||
|
|
@ -212,6 +266,16 @@ seed "swarm:S0VZ" "tok-1"
|
|||
run DAEMON_STARTED="Sat 2000-01-01 00:00:00.000000 UTC"
|
||||
expect "exit" $? 0
|
||||
expect "restart asked" "$(grep -c '^systemctl try-restart --no-block nix-daemon.service' "$dir/systemctl")" 1
|
||||
expect "restart claimed" "$(grep -c "cache.conf changed after nix-daemon.service last started — restarting it\$" "$dir/out")" 1
|
||||
|
||||
case="a daemon that never ran is not restarted, and nothing claims it was"
|
||||
reset
|
||||
seed "swarm:S0VZ" "tok-1"
|
||||
run DAEMON_STARTED=
|
||||
expect "exit" $? 0
|
||||
expect "fragment written" "$(grep -c "^extra-substituters" "$dir/nix/cache.conf")" 1
|
||||
expect "no restart" "$(restarts)" 0
|
||||
expect "no change-vs-start claim" "$(grep -c "changed after" "$dir/out")" 0
|
||||
|
||||
case="the same facts leave the fragment and the daemon alone"
|
||||
started=$(now)
|
||||
|
|
@ -225,9 +289,7 @@ started=$(now)
|
|||
seed "swarm:S0VZ" "tok-2"
|
||||
run DAEMON_STARTED="$started"
|
||||
expect "exit" $? 0
|
||||
expect "netrc" "$(body "$dir/attic/netrc")" "machine cache.example password tok-2"
|
||||
expect "client netrc follows" "$(body "$dir/nix/netrc")" "machine cache.example password tok-2"
|
||||
expect "client netrc mode" "$(mode "$dir/nix/netrc")" 640
|
||||
expect "netrc" "$(body "$dir/nix/netrc")" "machine cache.example password tok-2"
|
||||
expect "no restart" "$(restarts)" 0
|
||||
|
||||
case="a rotated key rewrites the fragment and restarts the daemon"
|
||||
|
|
@ -243,7 +305,6 @@ reset
|
|||
run DAEMON_STARTED="Sat 2000-01-01 00:00:00.000000 UTC"
|
||||
expect "exit" $? 0
|
||||
expect "no fragment" "$(body "$dir/nix/cache.conf")" missing
|
||||
expect "no client netrc" "$(body "$dir/nix/netrc")" missing
|
||||
expect "no restart" "$(restarts)" 0
|
||||
|
||||
case="a key that is refused writes no fragment"
|
||||
|
|
@ -252,7 +313,6 @@ seed "swarm:S0 VZ" "tok-1"
|
|||
run
|
||||
expect "exit" $? 1
|
||||
expect "no fragment" "$(body "$dir/nix/cache.conf")" missing
|
||||
expect "no client netrc" "$(body "$dir/nix/netrc")" missing
|
||||
|
||||
if [ "$failures" -ne 0 ]; then
|
||||
echo "$failures of $count checks failed" >&2
|
||||
|
|
|
|||
18
nix/script-tests/cache-pull-substitute.nix
Normal file
18
nix/script-tests/cache-pull-substitute.nix
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# `checks.script-test-cache-pull-substitute` — runs the closure pull
|
||||
# (`nix-store --realise <top> --max-jobs 0`, hive-c0re/src/cache_pull.rs)
|
||||
# against two file caches: a swarm cache that has the top path and lacks a
|
||||
# dependency, and the host's own substituter that has the dependency. The cases
|
||||
# are in ./cache-pull-substitute.sh.
|
||||
{ pkgs, ... }:
|
||||
pkgs.runCommand "hyperhive-script-test-cache-pull-substitute"
|
||||
{
|
||||
nativeBuildInputs = [
|
||||
pkgs.nix
|
||||
pkgs.coreutils
|
||||
pkgs.gnugrep
|
||||
];
|
||||
}
|
||||
''
|
||||
${pkgs.bash}/bin/bash ${./cache-pull-substitute.sh}
|
||||
touch "$out"
|
||||
''
|
||||
95
nix/script-tests/cache-pull-substitute.sh
Normal file
95
nix/script-tests/cache-pull-substitute.sh
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
# Cases for the closure pull (hive-c0re/src/cache_pull.rs): `nix-store --realise
|
||||
# <top> --max-jobs 0` against a store whose configuration is what the daemon
|
||||
# has on a hive host, the host's own substituter plus the swarm cache from the
|
||||
# nix.conf fragment.
|
||||
#
|
||||
# The swarm cache holds the top path and NOT its dependency, as attic does for
|
||||
# a path cache.nixos.org serves; a second cache, standing for cache.nixos.org,
|
||||
# holds the dependency. The stores are chroot stores, so the realise runs in
|
||||
# this process where a host runs it in the daemon; the substitution logic is
|
||||
# the same.
|
||||
set -uo pipefail
|
||||
|
||||
failures=0
|
||||
count=0
|
||||
case="setup"
|
||||
|
||||
fail() {
|
||||
echo "FAILED: $case: $*" >&2
|
||||
failures=$((failures + 1))
|
||||
}
|
||||
|
||||
expect() { # <description> <got> <want>
|
||||
count=$((count + 1))
|
||||
if [ "$2" != "$3" ]; then fail "$1: got '$2', wanted '$3'"; fi
|
||||
}
|
||||
|
||||
T=$(mktemp -d)
|
||||
export HOME=$T/home XDG_CONFIG_HOME=$T/home NIX_USER_CONF_FILES=
|
||||
mkdir -p "$HOME" "$T/empty"
|
||||
export NIX_CONF_DIR=$T/empty
|
||||
printf 'build-users-group =\nsandbox = false\nexperimental-features = nix-command\n' >"$T/empty/nix.conf"
|
||||
|
||||
src="local?root=$T/src"
|
||||
dep=$(nix eval --raw --store "$src" --expr 'builtins.toFile "dep" "dependency"')
|
||||
top=$(nix eval --raw --store "$src" --expr 'let dep = builtins.toFile "dep" "dependency"; in builtins.toFile "top" "needs ${dep}"')
|
||||
nix-store --generate-binary-cache-key swarm-test-1 "$T/swarm.sec" "$T/swarm.pub" >/dev/null
|
||||
nix-store --generate-binary-cache-key upstream-test-1 "$T/up.sec" "$T/up.pub" >/dev/null
|
||||
attic=file://$T/attic
|
||||
upstream=file://$T/upstream
|
||||
nix copy --from "$src" --to "$attic?secret-key=$T/swarm.sec" "$top"
|
||||
nix copy --from "$src" --to "$upstream?secret-key=$T/up.sec" "$dep"
|
||||
# What attic leaves out because the upstream serves it.
|
||||
rm -f "$T/attic/$(basename "$dep" | cut -c1-32).narinfo"
|
||||
|
||||
# The daemon's nix.conf: its own substituter and key, then the include.
|
||||
# conf <name> <fragment text>: a NIX_CONF_DIR with that fragment.
|
||||
conf() {
|
||||
mkdir -p "$T/$1"
|
||||
printf 'build-users-group =\nsandbox = false\nsubstituters = %s\ntrusted-public-keys = %s\n!include %s/cache.conf\n' \
|
||||
"$upstream" "$(cat "$T/up.pub")" "$T/$1" >"$T/$1/nix.conf"
|
||||
printf '%s\n' "$2" >"$T/$1/cache.conf"
|
||||
}
|
||||
fragment="extra-substituters = $attic
|
||||
extra-trusted-public-keys = $(cat "$T/swarm.pub")"
|
||||
# The paths are content-addressed (`builtins.toFile`), which `require-sigs` does
|
||||
# not apply to, so this test cannot show the signature gate; hive-priv's
|
||||
# `nix store verify` step and the fragment test carry the key.
|
||||
|
||||
# pull <conf dir> <dest name>: the exact pull of cache_pull.rs.
|
||||
pull() {
|
||||
NIX_CONF_DIR=$T/$1 nix-store --store "local?root=$T/$2" --realise "$top" --max-jobs 0 >/dev/null 2>"$T/err"
|
||||
}
|
||||
valid() { # <dest name> <path>
|
||||
nix-store --store "local?root=$T/$1" --check-validity "$2" 2>/dev/null && echo yes || echo no
|
||||
}
|
||||
|
||||
narinfo() { # <cache dir> <store path>: whether the cache has its narinfo
|
||||
if [ -e "$T/$1/$(basename "$2" | cut -c1-32).narinfo" ]; then echo yes; else echo no; fi
|
||||
}
|
||||
|
||||
case="control: the swarm cache has the top path and lacks the dependency"
|
||||
expect "attic has the top path" "$(narinfo attic "$top")" yes
|
||||
expect "attic lacks the dependency" "$(narinfo attic "$dep")" no
|
||||
expect "upstream has the dependency" "$(narinfo upstream "$dep")" yes
|
||||
|
||||
case="the client-side copy that failed on a host fails here too"
|
||||
if nix copy --from "$attic" --to "local?root=$T/d0" "$top" 2>"$T/err"; then
|
||||
fail "nix copy --from the swarm cache alone succeeded"
|
||||
fi
|
||||
expect "no top path" "$(valid d0 "$top")" no
|
||||
|
||||
case="the daemon with both substituters realises the top path and its dependency"
|
||||
conf both "$fragment"
|
||||
pull both d1
|
||||
expect "exit" $? 0
|
||||
expect "top path valid" "$(valid d1 "$top")" yes
|
||||
expect "dependency valid" "$(valid d1 "$dep")" yes
|
||||
|
||||
case="without the fragment the daemon has only the upstream and cannot realise the top path"
|
||||
conf plain ""
|
||||
if pull plain d2; then fail "realised without the swarm cache"; fi
|
||||
expect "no top path" "$(valid d2 "$top")" no
|
||||
|
||||
echo "$count checks, $failures failed"
|
||||
[ "$failures" = 0 ]
|
||||
|
|
@ -26,13 +26,17 @@ cat >"$dir/bin/systemctl" <<STUB
|
|||
args=" \$* "
|
||||
case "\$args" in
|
||||
*" show "*) printf '%s\n' "\$STARTED" ;;
|
||||
*" is-active "*) [ "\$ACTIVE" = 1 ] ;;
|
||||
*" is-active "*)
|
||||
case "\$args" in
|
||||
*" container@"*) [ "\$ACTIVE" = 1 ] ;;
|
||||
*) [ "\$UNIT_ACTIVE" = 1 ] ;;
|
||||
esac ;;
|
||||
*" is-failed "*) [ "\$FAILED" = 1 ] ;;
|
||||
*) printf '%s\n' "\$*" >>"$calls" ;;
|
||||
esac
|
||||
STUB
|
||||
chmod +x "$dir/bin/systemctl"
|
||||
export PATH="$dir/bin:$PATH" ACTIVE=1 FAILED=0 STARTED=
|
||||
export PATH="$dir/bin:$PATH" ACTIVE=1 UNIT_ACTIVE=1 FAILED=0 STARTED=
|
||||
|
||||
export LC_ALL=de_DE.UTF-8
|
||||
|
||||
|
|
@ -106,10 +110,21 @@ check "a failed unit is reset and started" \
|
|||
refresh_unit gitea-runner-swarm.service "$path"
|
||||
FAILED=0
|
||||
|
||||
STARTED=$(systemd_ts 1791232888.193158)
|
||||
got=$(refresh_unit gitea-runner-swarm.service "$path" 2>&1)
|
||||
case="control: a running unit is said to be restarted"
|
||||
count=$((count + 1))
|
||||
if [ "$got" != "$path changed after gitea-runner-swarm.service last started — restarting it" ]; then fail "logged: $got"; fi
|
||||
|
||||
STARTED=
|
||||
check "a unit never active is restarted" \
|
||||
"try-restart --no-block gitea-runner-swarm.service" "" \
|
||||
UNIT_ACTIVE=0
|
||||
check "a unit never active is not asked to restart" "" "" \
|
||||
refresh_unit gitea-runner-swarm.service "$path"
|
||||
got=$(refresh_unit gitea-runner-swarm.service "$path" 2>&1)
|
||||
case="a unit never active is not said to be restarted"
|
||||
count=$((count + 1))
|
||||
if [ -n "$got" ]; then fail "logged: $got"; fi
|
||||
UNIT_ACTIVE=1
|
||||
|
||||
ACTIVE=0
|
||||
check "nothing while the container is down" "" "" \
|
||||
|
|
|
|||
Loading…
Reference in a new issue