#!/usr/bin/env bash # deploy/lw-deploy.sh test: --yes generates edges, aggregators and a standalone # node for the manual runtime, plain and with TLS and scram, that bin/lw runs; # --yes fails closed, --dry-run changes nothing (manual, native, docker), and a # root host with docker and the logwisp:dev image runs the container too. # Requires: bash 5+, coreutils, curl, openssl. Linux dev host only. set -u . "$(dirname -- "${BASH_SOURCE[0]}")/lib.sh" RUN=$E2E_DIR/run-deploy DEPLOY=${E2E_DIR%/*}/deploy/lw-deploy.sh PKI=$RUN/pki OUT=$RUN/out APP=$RUN/app # the directory the edges tail SECRETS=$RUN/secrets PORT_CHAIN=15871 PORT_HTTP=15872 PORT_TCP=15873 PORT_SA_HTTP=15874 PORT_S_CHAIN=15875 PORT_S_HTTP=15876 PORT_D_CHAIN=15877 PORT_D_HTTP=15878 PORT_DRY=15879 # only written by the dry run, never bound PORTS="$PORT_CHAIN $PORT_HTTP $PORT_TCP $PORT_SA_HTTP $PORT_S_CHAIN $PORT_S_HTTP $PORT_D_CHAIN $PORT_D_HTTP" IMAGE=logwisp:dev CONTAINER=lw-e2e-deploy-aggregator e2e_init "$@" deploy() { # NAME ARGS...: lw-deploy.sh --yes, its output in $LOG/deploy-NAME.out local name=$1 shift "$DEPLOY" --yes "$@" >"$LOG/deploy-$name.out" 2>&1 } said() { grep -qF -- "$2" "$LOG/deploy-$1.out"; } # NAME TEXT: lw-deploy.sh printed TEXT declare -A GEN # NAME -> lw-deploy.sh's exit status generate() { deploy "$1" --runtime manual --config-dir "$RUN/$1" --bin "$BIN" "${@:2}"; GEN[$1]=$?; } section "Setup" need curl openssl [[ -x $DEPLOY ]] || abort "no $(short "$DEPLOY")" # A container an earlier run left (--keep) holds its published ports if [[ -d $RUN/docker ]] && command -v docker >/dev/null; then docker rm -f "$CONTAINER" >/dev/null 2>&1; fi ports_free $PORTS rm -rf "$RUN" mkdir -p "$LOG" "$APP" "$OUT" pki_ca pki_leaf aggregator aggregator.internal serverAuth "IP:127.0.0.1" echo "first line, there before the edges start" >"$APP/app.log" info "test PKI in $(short "$PKI")/; the edges tail $(short "$APP")/*.log" generate aggregator --role aggregator --listen 127.0.0.1 --chain-port $PORT_CHAIN \ --file-dir "$OUT/aggregator" --http-port $PORT_HTTP --tcp-port $PORT_TCP generate edge --role edge --log-dir "$APP" --from start --aggregator 127.0.0.1 --chain-port $PORT_CHAIN \ --node deploy-edge generate standalone --role standalone --log-dir "$APP" --from start --listen 127.0.0.1 \ --file-dir "$OUT/standalone" --http-port $PORT_SA_HTTP --format json SECURE_AGGREGATOR=(--role aggregator --listen 127.0.0.1 --chain http --chain-port $PORT_S_CHAIN --file-dir "$OUT/secure" --http-port $PORT_S_HTTP --tls --cert-file "$PKI/aggregator.crt" --key-file "$PKI/aggregator.key" --auth scram --add-user edge-01 --sink-auth scram --add-viewer viewer-01 --secrets-dir "$SECRETS") generate secure-aggregator "${SECURE_AGGREGATOR[@]}" generate secure-edge --role edge --log-dir "$APP" --from start --chain http --aggregator 127.0.0.1 \ --chain-port $PORT_S_CHAIN --node deploy-secure --tls --ca-file "$PKI/ca.crt" --auth scram \ --password-file "$SECRETS/edge-01.pass" info "generated with --yes --runtime manual: aggregator, edge, standalone, secure-aggregator, secure-edge" section "Startup" for name in aggregator standalone secure-aggregator edge secure-edge; do if [[ ${GEN[$name]} != 0 ]]; then tail -n 5 "$LOG/deploy-$name.out" | sed 's/^/ /' abort "lw-deploy.sh did not generate $name (exit ${GEN[$name]})" fi spawn "$name" "$BIN" -c "$RUN/$name/logwisp.toml" done for p in $PORT_CHAIN $PORT_HTTP $PORT_TCP $PORT_SA_HTTP $PORT_S_CHAIN $PORT_S_HTTP; do wait_port "$p" || abort "port $p is not listening" done daemons_up write_env LW="$BIN" CA="$PKI/ca.crt" PW="$SECRETS/viewer-01.pass" APP="$APP" guide "lw-deploy.sh test" < . $(short "$RUN")/env Append a line and watch it arrive: > curl -N http://127.0.0.1:$PORT_HTTP/stream > echo hello >>\$APP/app.log The secured sink (bearer keeps the token off argv): > token=\$(\$LW auth token --url https://127.0.0.1:$PORT_S_HTTP --user viewer-01 --password-file \$PW --ca-file \$CA) > curl -N --cacert \$CA -H @<(bearer) https://127.0.0.1:$PORT_S_HTTP/stream Logs: $(short "$LOG")/ EOF manual_hold total_processed() { # PORT: of a plain http sink curl -s --noproxy '*' --max-time 3 "http://127.0.0.1:$1/status" 2>/dev/null | grep -o '"total_processed":[0-9]*' | grep -o '[0-9]*$' || echo 0 } section "Generated configurations" for name in aggregator edge standalone secure-aggregator secure-edge; do conf="$RUN/$name/logwisp.toml" ok=0 "$BIN" --check -c "$conf" >>"$LOG/lw-check.out" 2>&1 && ok=1 check "$name: --yes wrote $(short "$conf") (exit ${GEN[$name]}), lw --check accepts it" $ok done section "Plain chain and outputs" wait_until 10 grep -q 'deploy-edge/app.log "first line' "$OUT/aggregator/logwisp.log" n=$(grep -c 'deploy-edge/app.log "first line' "$OUT/aggregator/logwisp.log") check "edge -> aggregator: the line there at start reached the file output ($n)" $((n == 1)) live="appended line $$" tcp_read $PORT_TCP 4 >"$RUN/tcp.out" & tcp_pid=$! curl -sN --noproxy '*' --max-time 4 "http://127.0.0.1:$PORT_HTTP/stream" >"$RUN/sse.out" 2>/dev/null & sse_pid=$! sleep 1 echo "$live" >>"$APP/app.log" wait "$tcp_pid" "$sse_pid" wait_until 10 grep -q "$live" "$OUT/aggregator/logwisp.log" n=$(grep -c "deploy-edge/app.log \"$live" "$OUT/aggregator/logwisp.log") check "edge -> aggregator: a line appended later reached the file output ($n)" $((n == 1)) n=$(grep -c "^data:.*deploy-edge/app.log \"$live" "$RUN/sse.out") check "aggregator http sink: /stream delivered it live ($n events)" $((n == 1)) n=$(grep -c "deploy-edge/app.log \"$live" "$RUN/tcp.out") check "aggregator tcp sink: delivered it live ($n lines)" $((n == 1)) n=$(total_processed $PORT_HTTP) check "aggregator http sink: /status counts both lines (total_processed=$n)" $((n >= 2)) wait_until 10 grep -q "$live" "$OUT/standalone/logwisp.log" n=$(grep -c -e '"fields":\["first line' -e "\"fields\":\[\"$live" "$OUT/standalone/logwisp.log") check "standalone: both lines in its file output, as json ($n)" $((n == 2)) n=$(total_processed $PORT_SA_HTTP) check "standalone http sink: /status counts them (total_processed=$n)" $((n >= 2)) section "TLS and scram" wait_until 20 grep -q "edge-01/app.log \"$live" "$OUT/secure/logwisp.log" n=$(grep -c 'edge-01/app.log' "$OUT/secure/logwisp.log") check "secure-edge -> secure-aggregator: entries over http_chain, TLS and scram ($n lines)" $((n >= 2)) n=$(grep -c 'deploy-secure/' "$OUT/secure/logwisp.log") check "secure-aggregator: the node label is the username, not the edge's own ($n deploy-secure lines)" $((n == 0)) modes="$(stat -c %a "$SECRETS/edge-01.pass" "$SECRETS/viewer-01.pass" 2>/dev/null | paste -sd' ')" ok=$(is "$modes" "600 600") grep -qx 'username = "edge-01"' "$RUN/secure-aggregator/users.toml" || ok=0 grep -qx 'username = "viewer-01"' "$RUN/secure-aggregator/viewers.toml" || ok=0 grep -q 'viewer-01' "$RUN/secure-aggregator/users.toml" && ok=0 check "secure-aggregator: passwords generated (modes $modes), edge-01 in users.toml, viewer-01 in viewers.toml" $ok ok=1 cmp -s "$PKI/aggregator.crt" "$RUN/secure-aggregator/tls/node.crt" || ok=0 cmp -s "$PKI/aggregator.key" "$RUN/secure-aggregator/tls/node.key" || ok=0 cmp -s "$PKI/ca.crt" "$RUN/secure-edge/tls/ca.crt" || ok=0 cmp -s "$SECRETS/edge-01.pass" "$RUN/secure-edge/chain.pass" || ok=0 check "certificates, key and the edge's password copied into the configuration directories" $ok token="$("$BIN" auth token -url "https://127.0.0.1:$PORT_S_HTTP" -user viewer-01 \ -password-file "$SECRETS/viewer-01.pass" -ca-file "$PKI/ca.crt" 2>>"$LOG/auth-cli.out")" code="$(curl -s -o /dev/null -w '%{http_code}' --max-time 5 --noproxy '*' --cacert "$PKI/ca.crt" \ -H @<(printf 'Authorization: Bearer %s\n' "$token") "https://127.0.0.1:$PORT_S_HTTP/status" 2>/dev/null)" check "secure-aggregator http sink: viewer-01's token opens /status (HTTP $code)" $(is "$code" 200) code="$(curl -s -o /dev/null -w '%{http_code}' --max-time 5 --noproxy '*' --cacert "$PKI/ca.crt" \ "https://127.0.0.1:$PORT_S_HTTP/status" 2>/dev/null)" check "secure-aggregator http sink: /status refused without a token (HTTP $code)" $(is "$code" 401) section "Re-running a deployment" conf="$RUN/secure-aggregator/logwisp.toml" before="$(cksum <"$conf")" pass_before="$(cksum <"$SECRETS/edge-01.pass")" deploy rerun "${SECURE_AGGREGATOR[@]}" --runtime manual --config-dir "$RUN/secure-aggregator" --bin "$BIN" rc=$? ok=$((rc != 0)) grep -q 'exists (--force replaces it)' "$LOG/deploy-rerun.out" || ok=0 [[ $(cksum <"$conf") == "$before" ]] || ok=0 check "without --force, --yes refuses to replace a configuration (exit $rc)" $ok deploy rerun-force "${SECURE_AGGREGATOR[@]}" --runtime manual --config-dir "$RUN/secure-aggregator" --bin "$BIN" --force rc=$? ok=$((rc == 0)) [[ -f $conf.bak && $(cksum <"$conf.bak") == "$before" ]] || ok=0 [[ $(cksum <"$SECRETS/edge-01.pass") == "$pass_before" ]] || ok=0 check "with --force: the old configuration kept as logwisp.toml.bak, the generated passwords reused (exit $rc)" $ok section "--yes fails closed" deploy fail-edge --runtime manual --role edge --config-dir "$RUN/fail-edge" --bin "$BIN" --log-dir "$APP" rc=$? ok=$((rc != 0)) grep -q 'missing value: --aggregator' "$LOG/deploy-fail-edge.out" || ok=0 [[ -e $RUN/fail-edge ]] && ok=0 check "an edge without --aggregator stops before writing anything (exit $rc)" $ok deploy fail-aggregator --runtime manual --role aggregator --config-dir "$RUN/fail-aggregator" --bin "$BIN" rc=$? ok=$((rc != 0)) grep -q 'no output: give --file-dir, --http-port or --tcp-port' "$LOG/deploy-fail-aggregator.out" || ok=0 [[ -e $RUN/fail-aggregator ]] && ok=0 check "an aggregator without an output stops before writing anything (exit $rc)" $ok deploy implied-auth --dry-run --runtime manual --role aggregator --config-dir "$RUN/dry/conf" --listen 127.0.0.1 \ --http-port $PORT_DRY --tls --cert-file "$PKI/aggregator.crt" --key-file "$PKI/aggregator.key" \ --ca-file "$PKI/ca.crt" --add-user edge-01 --sink-allow viewer-01 --secrets-dir "$RUN/dry/secrets" rc=$? ok=$((rc == 0)) said implied-auth "over tcp_chain (TLS, auth scram)" || ok=0 said implied-auth "/stream /status (txt, TLS, auth mtls)" || ok=0 check "without --auth and --sink-auth, --add-user implies scram and --sink-allow mtls (exit $rc)" $ok deploy fail-auth --runtime manual --role aggregator --config-dir "$RUN/fail-auth" --bin "$BIN" \ --file-dir "$RUN/fail-auth/out" --tls --cert-file "$PKI/aggregator.crt" --key-file "$PKI/aggregator.key" \ --auth none --add-user edge-01 --secrets-dir "$RUN/fail-auth/secrets" rc=$? ok=$((rc != 0)) said fail-auth '--add-user needs --auth scram' || ok=0 [[ -e $RUN/fail-auth ]] && ok=0 check "an --add-user that --auth none contradicts stops before writing anything (exit $rc)" $ok deploy fail-pattern --runtime manual --role standalone --config-dir "$RUN/fail-pattern" --bin "$BIN" --log-dir "$APP" \ --file-dir "$RUN/fail-pattern/out" --log-pattern $'*.log\nfrom = "start"' rc=$? ok=$((rc != 0)) said fail-pattern 'invalid --log-pattern' || ok=0 [[ -e $RUN/fail-pattern ]] && ok=0 check "a --log-pattern with a newline stops before writing anything (exit $rc)" $ok (cd "$RUN" && deploy fail-secrets --runtime manual --role aggregator --config-dir "$RUN/fail-secrets" \ --bin "$BIN" --file-dir "$RUN/fail-secrets/out" --tls --cert-file "$PKI/aggregator.crt" \ --key-file "$PKI/aggregator.key" --auth scram --add-user edge-01 --secrets-dir '~/secrets') rc=$? ok=$((rc != 0)) said fail-secrets 'not an absolute path: ~/secrets' || ok=0 [[ -e $RUN/fail-secrets || -e "$RUN/~" ]] && ok=0 check "a --secrets-dir of ~/secrets stops before writing anything (exit $rc)" $ok deploy fail-check --runtime manual --role aggregator --config-dir "$RUN/fail-check" --bin "$BIN" \ --file-dir "$RUN/fail-check/out" --tls --cert-file "$APP/app.log" --key-file "$APP/app.log" rc=$? ok=$((rc != 0)) said fail-check 'lw --check rejects' || ok=0 check "a configuration lw --check rejects (a log file as the certificate) stops the run (exit $rc)" $ok deploy fail-tmp --dry-run --runtime native --role standalone --log-dir "$APP" --file-dir /tmp/lw-deploy-test \ --bin "$BIN" rc=$? ok=$((rc != 0)) said fail-tmp 'PrivateTmp=yes' || ok=0 check "native Linux: a --file-dir under /tmp, which the unit makes private, stops the run (exit $rc)" $ok # A failing install stands in for the real one: a regression must not install onto this host mkdir -p "$RUN/stub" printf '#!/bin/sh\nexit 1\n' >"$RUN/stub/install" chmod +x "$RUN/stub/install" PATH="$RUN/stub:$PATH" deploy fail-os --runtime native --os freebsd --role standalone --log-dir "$APP" \ --file-dir "$RUN/fail-os" --bin "$BIN" rc=$? ok=$((rc != 0)) said fail-os 'needs a freebsd host' || ok=0 said fail-os '+ install' && ok=0 check "--os freebsd --runtime native on this Linux host stops before changing anything (exit $rc)" $ok section "--dry-run changes nothing" # What a dry run could touch: the run directory (minus what the daemons write) # and what a native install writes snapshot() { find "$RUN" \( -path "$LOG" -o -path "$OUT" \) -prune -o -printf '%p %s %T@ %m\n' | sort | cksum stat -c '%n %s %Y' /usr/local/bin/lw /etc/logwisp /etc/systemd/system/logwisp.service \ /etc/systemd/system/logwisp.service.d /etc/sysusers.d/logwisp.conf /etc/tmpfiles.d/logwisp.conf 2>&1 } before="$(snapshot)" deploy dry-manual --dry-run --runtime manual --role aggregator --config-dir "$RUN/dry/conf" --bin "$BIN" \ --listen 127.0.0.1 --http-port $PORT_DRY --file-dir "$RUN/dry/out" --tls --cert-file "$PKI/aggregator.crt" \ --key-file "$PKI/aggregator.key" --auth scram --add-user edge-01 --sink-auth scram --add-viewer viewer-01 \ --secrets-dir "$RUN/dry/secrets" rc=$? ok=$((rc == 0)) said dry-manual "+ write $RUN/dry/conf/logwisp.toml" || ok=0 said dry-manual "+ generate a password into $RUN/dry/secrets/edge-01.pass" || ok=0 [[ -e $RUN/dry || $(snapshot) != "$before" ]] && ok=0 check "manual: printed the configuration and the passwords it would generate, wrote nothing (exit $rc)" $ok deploy dry-zone --dry-run --runtime manual --role aggregator --config-dir "$RUN/dry/conf" --listen fe80::1%lo \ --http-port $PORT_DRY --file-dir "$RUN/dry/out" --tls --cert-file "$PKI/aggregator.crt" \ --key-file "$PKI/aggregator.key" --sink-auth scram --add-viewer viewer-01 --secrets-dir "$RUN/dry/secrets" rc=$? ok=$((rc == 0)) said dry-zone "-url https://[fe80::1%25lo]:$PORT_DRY " || ok=0 said dry-zone 'host = "fe80::1%lo"' || ok=0 check "a link-local listener: its URLs escape the zone as %25, its host keeps it (exit $rc)" $ok deploy dry-native --dry-run --runtime native --role edge --log-dir "$APP" --aggregator 127.0.0.1 \ --chain-port $PORT_CHAIN --node deploy-native --bin "$BIN" rc=$? ok=$((rc == 0)) said dry-native "+ install -m 0755 $BIN /usr/local/bin/lw" || ok=0 said dry-native "+ write /etc/logwisp/logwisp.toml" || ok=0 said dry-native "+ systemctl enable logwisp" || ok=0 [[ $(snapshot) != "$before" ]] && ok=0 check "native: printed the install, the configuration and systemctl, changed nothing (exit $rc)" $ok deploy dry-docker --dry-run --runtime docker --role aggregator --name "$CONTAINER" --image "$IMAGE" --no-build \ --config-dir "$RUN/docker/conf" --file-dir "$OUT/docker" --listen 127.0.0.1 \ --chain-port $PORT_D_CHAIN --http-port $PORT_D_HTTP rc=$? # The command breaks before each option; joined, it is one line cmd="$(sed -e ':a' -e '/\\$/N; s/\\\n//; ta' "$LOG/deploy-dry-docker.out" | tr -s ' ' | grep '^+ docker run -d ')" ok=$((rc == 0)) for want in "--name $CONTAINER" "--restart unless-stopped" "--read-only" "--cap-drop ALL" \ "--security-opt no-new-privileges" "-u 65532:65532" "-v $RUN/docker/conf:/etc/logwisp:ro" \ "-v $OUT/docker:$OUT/docker" "-p 127.0.0.1:$PORT_D_CHAIN:$PORT_D_CHAIN" \ "-p 127.0.0.1:$PORT_D_HTTP:$PORT_D_HTTP" "$IMAGE -c /etc/logwisp/logwisp.toml"; do [[ $cmd == *" $want"* ]] || { ok=0; info "the docker run command lacks: $want"; } done said dry-docker '| host = "0.0.0.0"' || ok=0 # The snapshot leaves out $OUT, where its --file-dir lies [[ -e $OUT/docker || $(snapshot) != "$before" ]] && ok=0 check "docker: a hardened docker run publishing on --listen, the container binding 0.0.0.0, nothing written (exit $rc)" $ok section "Docker runtime" if ! command -v docker >/dev/null || ! docker info >/dev/null 2>&1; then skip "docker: no docker daemon here" elif ((EUID != 0)); then skip "docker: lw-deploy.sh needs root to give the configuration to the image user" elif ! docker image inspect "$IMAGE" >/dev/null 2>&1; then skip "docker: no $IMAGE image here (make image)" else at_exit "docker rm -f $CONTAINER" deploy docker --runtime docker --role aggregator --name "$CONTAINER" --image "$IMAGE" --no-build \ --config-dir "$RUN/docker/conf" --file-dir "$OUT/docker" --listen 127.0.0.1 \ --chain-port $PORT_D_CHAIN --http-port $PORT_D_HTTP rc=$? check "docker: the aggregator container is running (exit $rc)" $(is "$rc" 0) generate docker-edge --role edge --log-dir "$APP" --aggregator 127.0.0.1 --chain-port $PORT_D_CHAIN \ --node deploy-docker spawn docker-edge "$BIN" -c "$RUN/docker-edge/logwisp.toml" for i in $(seq 1 15); do echo "line $i for the container" >>"$APP/app.log" sleep 1 grep -q 'deploy-docker/app.log' "$OUT/docker/logwisp.log" 2>/dev/null && break done n=$(grep -c 'deploy-docker/app.log' "$OUT/docker/logwisp.log" 2>/dev/null) check "docker: a native edge's entries reached the container's file output (${n:-0} lines)" $((${n:-0} >= 1)) n=$(total_processed $PORT_D_HTTP) check "docker: the published http sink answers /status (total_processed=$n)" $((n >= 1)) fi summary