run attic as a swarm service with deploy.attic.enable
This commit is contained in:
parent
f59ed01353
commit
458a046b8d
2 changed files with 1242 additions and 66 deletions
41
nix/script-tests/attic-init.nix
Normal file
41
nix/script-tests/attic-init.nix
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
# `checks.script-test-attic-init` — runs ../host-modules/lib/attic-init.nix's two
|
||||
# scripts, as the cache's host units run them, against stub `bao`, `openssl`,
|
||||
# `atticadm`, `attic`, `curl` and `systemctl`. The cases are in ./attic-init.sh.
|
||||
#
|
||||
# What runs is the rendered script, behind the same helpers the units put in
|
||||
# front of it.
|
||||
{
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
let
|
||||
init = import ../host-modules/lib/attic-init.nix { inherit (pkgs) lib; } {
|
||||
jwtPath = "secret/swarm/services/attic/jwt-secret";
|
||||
pullPath = "secret/swarm/services/attic/pull-token";
|
||||
pushPath = "secret/swarm/services/attic/push-token";
|
||||
cache = "grid";
|
||||
endpoint = "http://127.0.0.1:8910";
|
||||
configFile = "@DIR@/server.toml";
|
||||
envFile = "@DIR@/secrets/server.env";
|
||||
machine = "swarm-attic";
|
||||
unit = "swarm-attic";
|
||||
};
|
||||
render =
|
||||
name: body:
|
||||
pkgs.writeText name ''
|
||||
set -euo pipefail
|
||||
${import ../host-modules/lib/atomic-write-secret.nix { }}
|
||||
${import ../host-modules/lib/refresh-consumer.nix { }}
|
||||
${init.prelude}
|
||||
${body}
|
||||
'';
|
||||
in
|
||||
pkgs.runCommand "hyperhive-script-test-attic-init"
|
||||
{
|
||||
ENSURE_JWT = render "ensure-jwt.sh" init.ensureJwt;
|
||||
PROVISION = render "provision.sh" init.provision;
|
||||
}
|
||||
''
|
||||
${pkgs.bash}/bin/bash ${./attic-init.sh}
|
||||
touch "$out"
|
||||
''
|
||||
232
nix/script-tests/attic-init.sh
Normal file
232
nix/script-tests/attic-init.sh
Normal file
|
|
@ -0,0 +1,232 @@
|
|||
# Cases for ../host-modules/lib/attic-init.nix. Each case runs the key script or
|
||||
# the cache script against stubs that record every call, over a store kept in
|
||||
# files, and compares the record and what the script left behind.
|
||||
set -uo pipefail
|
||||
|
||||
failures=0
|
||||
count=0
|
||||
case="setup"
|
||||
|
||||
fail() {
|
||||
echo "FAILED: $case: $*" >&2
|
||||
failures=$((failures + 1))
|
||||
}
|
||||
|
||||
dir=$(mktemp -d)
|
||||
mkdir -p "$dir/bin" "$dir/store" "$dir/secrets"
|
||||
|
||||
# The scripts name their files under @DIR@, which is this run's directory.
|
||||
for s in ENSURE_JWT PROVISION; do
|
||||
sed "s|@DIR@|$dir|g" "${!s}" >"$dir/$s.sh"
|
||||
export "$s=$dir/$s.sh"
|
||||
done
|
||||
calls=$dir/calls
|
||||
export DIR=$dir
|
||||
|
||||
# The store is a directory with one file per path. `get` exits 2 on a miss, as
|
||||
# `bao kv get` does. `put -cas=0` refuses an existing path; without it a put
|
||||
# overwrites. $RACE_PATH makes another run create that path just before our put
|
||||
# lands.
|
||||
cat >"$dir/bin/bao" <<STUB
|
||||
#!$BASH
|
||||
file() { printf '%s/store/%s' "\$DIR" "\$(printf '%s' "\$1" | tr / _)"; }
|
||||
case "\$1 \$2" in
|
||||
"login -method=cert")
|
||||
echo "bao login" >>"$calls"
|
||||
if [ "\${LOGIN_FAIL:-0}" = 1 ]; then echo denied >&2; exit 2; fi
|
||||
echo store-token
|
||||
;;
|
||||
"kv get")
|
||||
path=\${4}
|
||||
f=\$(file "\$path")
|
||||
echo "bao get \$path" >>"$calls"
|
||||
[ -f "\$f" ] || exit 2
|
||||
cat "\$f"
|
||||
;;
|
||||
"kv put")
|
||||
if [ "\$3" = "-cas=0" ]; then cas=1; path=\$4; else cas=0; path=\$3; fi
|
||||
f=\$(file "\$path")
|
||||
echo "bao put cas=\$cas \$path" >>"$calls"
|
||||
value=\$(cat)
|
||||
if [ "\${RACE_PATH:-}" = "\$path" ] && [ ! -f "\$f" ]; then
|
||||
printf '%s' raced >"\$f"
|
||||
fi
|
||||
if [ "\$cas" = 1 ] && [ -f "\$f" ]; then echo "check-and-set parameter did not match" >&2; exit 1; fi
|
||||
printf '%s' "\$value" >"\$f"
|
||||
;;
|
||||
esac
|
||||
STUB
|
||||
|
||||
# The key is a fixed word, so the file the script writes is predictable.
|
||||
cat >"$dir/bin/openssl" <<STUB
|
||||
#!$BASH
|
||||
echo openssl >>"$calls"
|
||||
echo FAKEPEM
|
||||
STUB
|
||||
|
||||
# A token carries the subject and a serial, so a case can tell which one a call
|
||||
# used. The key comes in the environment, as the real tool takes it.
|
||||
cat >"$dir/bin/atticadm" <<STUB
|
||||
#!$BASH
|
||||
[ -n "\${ATTIC_SERVER_TOKEN_RS256_SECRET_BASE64:-}" ] || { echo "no key in the environment" >&2; exit 1; }
|
||||
echo "atticadm \$*" >>"$calls"
|
||||
n=\$(grep -c '^atticadm' "$calls")
|
||||
sub=\$2
|
||||
[ "\$1" = "-f" ] && sub=\$5
|
||||
echo "tok-\$sub-\$n"
|
||||
STUB
|
||||
|
||||
# The cache exists once \`cache create\` ran. Each call records the token the
|
||||
# client config names.
|
||||
cat >"$dir/bin/attic" <<STUB
|
||||
#!$BASH
|
||||
tokenfile=\$(sed -n 's/^token-file = "\(.*\)"$/\1/p' "\$XDG_CONFIG_HOME/attic/config.toml")
|
||||
token=\$(cat "\$tokenfile")
|
||||
echo "attic \$* | token=\$token" >>"$calls"
|
||||
case "\$1 \$2" in
|
||||
"cache info") [ -f "\$DIR/cache-exists" ] ;;
|
||||
"cache create") touch "\$DIR/cache-exists" ;;
|
||||
*) exit 1 ;;
|
||||
esac
|
||||
STUB
|
||||
chmod +x "$dir/bin/"*
|
||||
|
||||
cat >"$dir/bin/curl" <<STUB
|
||||
#!$BASH
|
||||
echo curl >>"$calls"
|
||||
[ "\${CURL_FAIL:-0}" != 1 ]
|
||||
STUB
|
||||
printf '#!%s\nexit 0\n' "$BASH" >"$dir/bin/sleep"
|
||||
# A container that is not running has nothing to refresh.
|
||||
printf '#!%s\nexit 1\n' "$BASH" >"$dir/bin/systemctl"
|
||||
chmod +x "$dir/bin/"*
|
||||
|
||||
export PATH="$dir/bin:$PATH"
|
||||
|
||||
# n <pattern>: how many recorded calls match.
|
||||
n() { grep -c -- "$1" "$calls" || true; }
|
||||
|
||||
# run <script> [VAR=value…]: the script's exit status.
|
||||
run() {
|
||||
local script=$1
|
||||
shift
|
||||
env "$@" bash "$script" >"$dir/out" 2>"$dir/err"
|
||||
}
|
||||
|
||||
reset() {
|
||||
rm -rf "$dir/store" "$dir/secrets" "$dir/cache-exists" "$calls"
|
||||
mkdir -p "$dir/store" "$dir/secrets"
|
||||
: >"$calls"
|
||||
}
|
||||
|
||||
stored() { cat "$dir/store/$1" 2>/dev/null || true; }
|
||||
JWT=secret_swarm_services_attic_jwt-secret
|
||||
PULL=secret_swarm_services_attic_pull-token
|
||||
PUSH=secret_swarm_services_attic_push-token
|
||||
ENVFILE=$dir/secrets/server.env
|
||||
|
||||
expect() { # <description> <got> <want>
|
||||
count=$((count + 1))
|
||||
if [ "$2" != "$3" ]; then fail "$1: got '$2', wanted '$3'"; fi
|
||||
}
|
||||
|
||||
# ── the key ──────────────────────────────────────────────────────────────
|
||||
case="first start creates the key and hands it over"
|
||||
reset
|
||||
run "$ENSURE_JWT"
|
||||
expect "exit status" $? 0
|
||||
expect "key minted once" "$(n '^openssl')" 1
|
||||
expect "stored key" "$(stored $JWT)" "$(printf 'FAKEPEM\n' | base64 -w0)"
|
||||
expect "env file" "$(cat "$ENVFILE")" "ATTIC_SERVER_TOKEN_RS256_SECRET_BASE64=$(stored $JWT)"
|
||||
expect "env file mode" "$(stat -c %a "$ENVFILE")" 400
|
||||
|
||||
case="a second start mints nothing and keeps the key"
|
||||
first=$(stored $JWT)
|
||||
run "$ENSURE_JWT"
|
||||
expect "exit status" $? 0
|
||||
expect "key minted still once" "$(n '^openssl')" 1
|
||||
expect "no second create" "$(n '^bao put')" 1
|
||||
expect "stored key unchanged" "$(stored $JWT)" "$first"
|
||||
|
||||
case="a stored key is used, not replaced, when the file is gone"
|
||||
rm -f "$ENVFILE"
|
||||
run "$ENSURE_JWT"
|
||||
expect "exit status" $? 0
|
||||
expect "key minted still once" "$(n '^openssl')" 1
|
||||
expect "env file rewritten from the store" "$(cat "$ENVFILE")" "ATTIC_SERVER_TOKEN_RS256_SECRET_BASE64=$first"
|
||||
|
||||
case="a create lost to another run uses the winner's key"
|
||||
reset
|
||||
run "$ENSURE_JWT" RACE_PATH=secret/swarm/services/attic/jwt-secret
|
||||
expect "exit status" $? 0
|
||||
expect "stored key is the winner's" "$(stored $JWT)" raced
|
||||
expect "env file has the winner's" "$(cat "$ENVFILE")" "ATTIC_SERVER_TOKEN_RS256_SECRET_BASE64=raced"
|
||||
|
||||
case="a refused login stops before anything is minted"
|
||||
reset
|
||||
run "$ENSURE_JWT" LOGIN_FAIL=1
|
||||
expect "exit status" $? 1
|
||||
expect "nothing minted" "$(n '^openssl')" 0
|
||||
expect "nothing written" "$(ls "$dir/secrets" | wc -l)" 0
|
||||
|
||||
# ── the cache and the tokens ─────────────────────────────────────────────
|
||||
case="first start creates the cache and both tokens"
|
||||
reset
|
||||
run "$ENSURE_JWT"
|
||||
: >"$calls"
|
||||
run "$PROVISION"
|
||||
expect "exit status" $? 0
|
||||
expect "pull token stored" "$(stored $PULL)" "tok-swarm-hives-pull-1"
|
||||
expect "push token stored" "$(stored $PUSH)" "tok-swarm-builder-push-2"
|
||||
expect "cache created once" "$(n '^attic cache create')" 1
|
||||
expect "cache created by the short-lived token" "$(n '^attic cache create local:grid | token=tok-swarm-attic-init-3')" 1
|
||||
expect "tokens minted: pull, push, the creator" "$(n '^atticadm')" 3
|
||||
expect "pull token reads only the cache" "$(n 'make-token --sub swarm-hives-pull --validity 10y --pull grid$')" 1
|
||||
expect "push token pulls and pushes" "$(n 'make-token --sub swarm-builder-push --validity 10y --pull grid --push grid$')" 1
|
||||
expect "the creator may create and pull, for minutes" "$(n 'make-token --sub swarm-attic-init --validity 5m --create-cache grid --pull grid$')" 1
|
||||
|
||||
case="a second start mints nothing and creates nothing"
|
||||
pull=$(stored $PULL)
|
||||
push=$(stored $PUSH)
|
||||
: >"$calls"
|
||||
run "$PROVISION"
|
||||
expect "exit status" $? 0
|
||||
expect "no token minted" "$(n '^atticadm')" 0
|
||||
expect "no create call" "$(n '^bao put')" 0
|
||||
expect "no cache create" "$(n '^attic cache create')" 0
|
||||
expect "the pull token looked the cache up" "$(n "^attic cache info local:grid | token=$pull")" 1
|
||||
expect "pull token unchanged" "$(stored $PULL)" "$pull"
|
||||
expect "push token unchanged" "$(stored $PUSH)" "$push"
|
||||
|
||||
case="an empty server gets its cache back with the stored tokens"
|
||||
rm -f "$dir/cache-exists"
|
||||
: >"$calls"
|
||||
run "$PROVISION"
|
||||
expect "exit status" $? 0
|
||||
expect "only the creator is minted" "$(n '^atticadm')" 1
|
||||
expect "no secret rewritten" "$(n '^bao put')" 0
|
||||
expect "cache created" "$(n '^attic cache create')" 1
|
||||
expect "pull token unchanged" "$(stored $PULL)" "$pull"
|
||||
|
||||
case="no tokens are minted while the server is down"
|
||||
reset
|
||||
run "$ENSURE_JWT"
|
||||
: >"$calls"
|
||||
run "$PROVISION" CURL_FAIL=1
|
||||
expect "exit status" $? 1
|
||||
expect "nothing minted" "$(n '^atticadm')" 0
|
||||
expect "nothing stored" "$(n '^bao put')" 0
|
||||
|
||||
case="the client reads its token from a file, not from an argument"
|
||||
reset
|
||||
run "$ENSURE_JWT"
|
||||
run "$PROVISION"
|
||||
expect "no token among the client's arguments" "$(grep -c -- '^attic [^|]*tok-' "$calls" || true)" 0
|
||||
# The control: the client did see tokens, through its config file.
|
||||
expect "tokens seen through the config file" "$(grep -c -- '| token=tok-' "$calls" || true)" 3
|
||||
|
||||
if [ "$failures" -ne 0 ]; then
|
||||
echo "$failures of $count checks failed" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "$count checks passed"
|
||||
Loading…
Reference in a new issue