zebra/docker/entrypoint.sh

343 lines
12 KiB
Bash
Executable File

#!/usr/bin/env bash
# Entrypoint for running Zebra in Docker.
#
# The main script logic is at the bottom.
#
# ## Notes
#
# - `$ZEBRA_CONF_PATH` can point to an existing Zebra config file, or if not set,
# the script will look for a default config at ${HOME}/.config/zebrad.toml,
# or generate one using environment variables.
set -eo pipefail
# These are the default cached state directories for Zebra and lightwalletd.
#
# They are set to `${HOME}/.cache/zebra` and `${HOME}/.cache/lwd`
# respectively, but can be overridden by setting the
# `ZEBRA_CACHE_DIR` and `LWD_CACHE_DIR` environment variables.
: "${ZEBRA_CACHE_DIR:=${HOME}/.cache/zebra}"
: "${LWD_CACHE_DIR:=${HOME}/.cache/lwd}"
: "${ZEBRA_COOKIE_DIR:=${HOME}/.cache/zebra}"
# Use gosu to drop privileges and execute the given command as the specified UID:GID
exec_as_user() {
user=$(id -u)
if [[ ${user} == '0' ]]; then
exec gosu "${UID}:${GID}" "$@"
else
exec "$@"
fi
}
# Modifies the Zebra config file using environment variables.
#
# This function generates a new config file from scratch at ZEBRA_CONF_PATH
# using the provided environment variables.
#
# It creates a complete configuration with network settings, state, RPC,
# metrics, tracing, and mining sections based on environment variables.
prepare_conf_file() {
# Base configuration
cat >"${ZEBRA_CONF_PATH}" <<EOF
[network]
network = "${NETWORK:=Mainnet}"
listen_addr = "0.0.0.0"
cache_dir = "${ZEBRA_CACHE_DIR}"
[state]
cache_dir = "${ZEBRA_CACHE_DIR}"
$( [[ -n ${ZEBRA_RPC_PORT} ]] && cat <<-SUB_EOF
[rpc]
listen_addr = "${RPC_LISTEN_ADDR:=0.0.0.0}:${ZEBRA_RPC_PORT}"
enable_cookie_auth = ${ENABLE_COOKIE_AUTH:=true}
$( [[ -n ${ZEBRA_COOKIE_DIR} ]] && echo "cookie_dir = \"${ZEBRA_COOKIE_DIR}\"" )
SUB_EOF
)
$( [[ " ${FEATURES} " =~ " prometheus " ]] && cat <<-SUB_EOF
[metrics]
endpoint_addr = "${METRICS_ENDPOINT_ADDR:=0.0.0.0}:${METRICS_ENDPOINT_PORT:=9999}"
SUB_EOF
)
$( [[ -n ${LOG_FILE} || -n ${LOG_COLOR} || -n ${TRACING_ENDPOINT_ADDR} || -n ${USE_JOURNALD} ]] && cat <<-SUB_EOF
[tracing]
$( [[ -n ${USE_JOURNALD} ]] && echo "use_journald = ${USE_JOURNALD}" )
$( [[ " ${FEATURES} " =~ " filter-reload " ]] && echo "endpoint_addr = \"${TRACING_ENDPOINT_ADDR:=0.0.0.0}:${TRACING_ENDPOINT_PORT:=3000}\"" )
$( [[ -n ${LOG_FILE} ]] && echo "log_file = \"${LOG_FILE}\"" )
$( [[ ${LOG_COLOR} == "true" ]] && echo "force_use_color = true" )
$( [[ ${LOG_COLOR} == "false" ]] && echo "use_color = false" )
SUB_EOF
)
$( [[ -n ${MINER_ADDRESS} ]] && cat <<-SUB_EOF
[mining]
miner_address = "${MINER_ADDRESS}"
SUB_EOF
)
EOF
# Ensure the config file itself has the correct ownership
#
# This is safe in this context because prepare_conf_file is called only when
# ZEBRA_CONF_PATH is not set, and there's no file mounted at that path.
chown "${UID}:${GID}" "${ZEBRA_CONF_PATH}" || exit_error "Failed to secure config file: ${ZEBRA_CONF_PATH}"
}
# Helper function
exit_error() {
echo "$1" >&2
exit 1
}
# Creates a directory if it doesn't exist and sets ownership to specified UID:GID.
# Also ensures the parent directories have the correct ownership.
#
# ## Parameters
#
# - $1: Directory path to create and own
create_owned_directory() {
local dir="$1"
# Skip if directory is empty
[[ -z ${dir} ]] && return
# Create directory with parents
mkdir -p "${dir}" || exit_error "Failed to create directory: ${dir}"
# Set ownership for the created directory
chown -R "${UID}:${GID}" "${dir}" || exit_error "Failed to secure directory: ${dir}"
# Set ownership for parent directory (but not if it's root or home)
local parent_dir
parent_dir="$(dirname "${dir}")"
if [[ "${parent_dir}" != "/" && "${parent_dir}" != "${HOME}" ]]; then
chown "${UID}:${GID}" "${parent_dir}"
fi
}
# Create and own cache and config directories
[[ -n ${ZEBRA_CACHE_DIR} ]] && create_owned_directory "${ZEBRA_CACHE_DIR}"
[[ -n ${LWD_CACHE_DIR} ]] && create_owned_directory "${LWD_CACHE_DIR}"
[[ -n ${ZEBRA_COOKIE_DIR} ]] && create_owned_directory "${ZEBRA_COOKIE_DIR}"
[[ -n ${LOG_FILE} ]] && create_owned_directory "$(dirname "${LOG_FILE}")"
# Runs cargo test with an arbitrary number of arguments.
#
# Positional Parameters
#
# - '$1' must contain cargo FEATURES as described here:
# https://doc.rust-lang.org/cargo/reference/features.html#command-line-feature-options
# - The remaining params will be appended to a command starting with
# `exec_as_user cargo test ... -- ...`
run_cargo_test() {
# Shift the first argument, as it's already included in the cmd
local features="$1"
shift
# Start constructing the command array
local cmd=(cargo test --locked --release --features "${features}" --package zebrad --test acceptance -- --nocapture --include-ignored)
# Loop through the remaining arguments
for arg in "$@"; do
if [[ -n ${arg} ]]; then
# If the argument is non-empty, add it to the command
cmd+=("${arg}")
fi
done
echo "Running: ${cmd[*]}"
# Execute directly to become PID 1
exec_as_user "${cmd[@]}"
}
# Runs tests depending on the env vars.
#
# ## Positional Parameters
#
# - $@: Arbitrary command that will be executed if no test env var is set.
run_tests() {
if [[ "${RUN_ALL_TESTS}" -eq "1" ]]; then
# Run unit, basic acceptance tests, and ignored tests, only showing command
# output if the test fails. If the lightwalletd environment variables are
# set, we will also run those tests.
exec_as_user cargo test --locked --release --workspace --features "${FEATURES}" \
-- --nocapture --include-ignored --skip check_no_git_refs_in_cargo_lock
elif [[ "${RUN_CHECK_NO_GIT_REFS}" -eq "1" ]]; then
# Run the check_no_git_refs_in_cargo_lock test.
exec_as_user cargo test --locked --release --workspace --features "${FEATURES}" \
-- --nocapture --include-ignored check_no_git_refs_in_cargo_lock
elif [[ "${TEST_FAKE_ACTIVATION_HEIGHTS}" -eq "1" ]]; then
# Run state tests with fake activation heights.
exec_as_user cargo test --locked --release --lib --features "zebra-test" \
--package zebra-state \
-- --nocapture --include-ignored with_fake_activation_heights
elif [[ "${TEST_SCANNER}" -eq "1" ]]; then
# Test the scanner.
exec_as_user cargo test --locked --release --package zebra-scan \
-- --nocapture --include-ignored scan_task_commands scan_start_where_left
elif [[ "${TEST_ZEBRA_EMPTY_SYNC}" -eq "1" ]]; then
# Test that Zebra syncs and checkpoints a few thousand blocks from an empty
# state.
run_cargo_test "${FEATURES}" "sync_large_checkpoints_"
elif [[ -n "${FULL_SYNC_MAINNET_TIMEOUT_MINUTES}" ]]; then
# Run a Zebra full sync test on mainnet.
run_cargo_test "${FEATURES}" "full_sync_mainnet"
elif [[ -n "${FULL_SYNC_TESTNET_TIMEOUT_MINUTES}" ]]; then
# Run a Zebra full sync test on testnet.
run_cargo_test "${FEATURES}" "full_sync_testnet"
elif [[ "${TEST_DISK_REBUILD}" -eq "1" ]]; then
# Run a Zebra sync up to the mandatory checkpoint.
run_cargo_test "${FEATURES} test_sync_to_mandatory_checkpoint_${NETWORK,,}" \
"sync_to_mandatory_checkpoint_${NETWORK,,}"
echo "ran test_disk_rebuild"
elif [[ "${TEST_UPDATE_SYNC}" -eq "1" ]]; then
# Run a Zebra sync starting at the cached tip, and syncing to the latest
# tip.
run_cargo_test "${FEATURES}" "zebrad_update_sync"
elif [[ "${TEST_CHECKPOINT_SYNC}" -eq "1" ]]; then
# Run a Zebra sync starting at the cached mandatory checkpoint, and syncing
# past it.
run_cargo_test "${FEATURES} test_sync_past_mandatory_checkpoint_${NETWORK,,}" \
"sync_past_mandatory_checkpoint_${NETWORK,,}"
elif [[ "${GENERATE_CHECKPOINTS_MAINNET}" -eq "1" ]]; then
# Generate checkpoints after syncing Zebra from a cached state on mainnet.
#
# TODO: disable or filter out logs like:
# test generate_checkpoints_mainnet has been running for over 60 seconds
run_cargo_test "${FEATURES}" "generate_checkpoints_mainnet"
elif [[ "${GENERATE_CHECKPOINTS_TESTNET}" -eq "1" ]]; then
# Generate checkpoints after syncing Zebra on testnet.
#
# This test might fail if testnet is unstable.
run_cargo_test "${FEATURES}" "generate_checkpoints_testnet"
elif [[ "${TEST_LWD_RPC_CALL}" -eq "1" ]]; then
# Starting at a cached Zebra tip, test a JSON-RPC call to Zebra.
# Run both the fully synced RPC test and the subtree snapshot test, one test
# at a time. Since these tests use the same cached state, a state problem in
# the first test can fail the second test.
run_cargo_test "${FEATURES}" "--test-threads" "1" "fully_synced_rpc_"
elif [[ "${TEST_LWD_INTEGRATION}" -eq "1" ]]; then
# Test launching lightwalletd with an empty lightwalletd and Zebra state.
run_cargo_test "${FEATURES}" "lightwalletd_integration"
elif [[ "${TEST_LWD_FULL_SYNC}" -eq "1" ]]; then
# Starting at a cached Zebra tip, run a lightwalletd sync to tip.
run_cargo_test "${FEATURES}" "lightwalletd_full_sync"
elif [[ "${TEST_LWD_UPDATE_SYNC}" -eq "1" ]]; then
# Starting with a cached Zebra and lightwalletd tip, run a quick update sync.
run_cargo_test "${FEATURES}" "lightwalletd_update_sync"
# These tests actually use gRPC.
elif [[ "${TEST_LWD_GRPC}" -eq "1" ]]; then
# Starting with a cached Zebra and lightwalletd tip, test all gRPC calls to
# lightwalletd, which calls Zebra.
run_cargo_test "${FEATURES}" "lightwalletd_wallet_grpc_tests"
elif [[ "${TEST_LWD_TRANSACTIONS}" -eq "1" ]]; then
# Starting with a cached Zebra and lightwalletd tip, test sending
# transactions gRPC call to lightwalletd, which calls Zebra.
run_cargo_test "${FEATURES}" "sending_transactions_using_lightwalletd"
# These tests use mining code, but don't use gRPC.
elif [[ "${TEST_GET_BLOCK_TEMPLATE}" -eq "1" ]]; then
# Starting with a cached Zebra tip, test getting a block template from
# Zebra's RPC server.
run_cargo_test "${FEATURES}" "get_block_template"
elif [[ "${TEST_SUBMIT_BLOCK}" -eq "1" ]]; then
# Starting with a cached Zebra tip, test sending a block to Zebra's RPC
# port.
run_cargo_test "${FEATURES}" "submit_block"
else
exec_as_user "$@"
fi
}
# Main Script Logic
#
# 1. First check if ZEBRA_CONF_PATH is explicitly set or if a file exists at that path
# 2. If not set but default config exists, use that
# 3. If neither exists, generate a default config at ${HOME}/.config/zebrad.toml
# 4. Print environment variables and config for debugging
# 5. Process command-line arguments and execute appropriate action
if [[ -n ${ZEBRA_CONF_PATH} ]]; then
if [[ -f ${ZEBRA_CONF_PATH} ]]; then
echo "ZEBRA_CONF_PATH was set to ${ZEBRA_CONF_PATH} and a file exists."
echo "Using user-provided config file"
else
echo "ERROR: ZEBRA_CONF_PATH was set and no config file found at ${ZEBRA_CONF_PATH}."
echo "Please ensure a config file exists or set ZEBRA_CONF_PATH to point to your config file."
exit 1
fi
else
if [[ -f "${HOME}/.config/zebrad.toml" ]]; then
echo "ZEBRA_CONF_PATH was not set."
echo "Using default config at ${HOME}/.config/zebrad.toml"
ZEBRA_CONF_PATH="${HOME}/.config/zebrad.toml"
else
echo "ZEBRA_CONF_PATH was not set and no default config found at ${HOME}/.config/zebrad.toml"
echo "Preparing a default one..."
ZEBRA_CONF_PATH="${HOME}/.config/zebrad.toml"
create_owned_directory "$(dirname "${ZEBRA_CONF_PATH}")"
prepare_conf_file
fi
fi
echo "INFO: Using the following environment variables:"
printenv
echo "Using Zebra config at ${ZEBRA_CONF_PATH}:"
cat "${ZEBRA_CONF_PATH}"
# - If "$1" is "--", "-", or "zebrad", run `zebrad` with the remaining params.
# - If "$1" is "test":
# - and "$2" is "zebrad", run `zebrad` with the remaining params,
# - else run tests with the remaining params.
# - TODO: If "$1" is "monitoring", start a monitoring node.
# - If "$1" doesn't match any of the above, run "$@" directly.
case "$1" in
--* | -* | zebrad)
shift
exec_as_user zebrad --config "${ZEBRA_CONF_PATH}" "$@"
;;
test)
shift
if [[ "$1" == "zebrad" ]]; then
shift
exec_as_user zebrad --config "${ZEBRA_CONF_PATH}" "$@"
else
run_tests "$@"
fi
;;
monitoring)
# TODO: Impl logic for starting a monitoring node.
:
;;
*)
exec_as_user "$@"
;;
esac