mirror of https://github.com/lukechilds/umbrel.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
233 lines
6.8 KiB
233 lines
6.8 KiB
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
UMBREL_ROOT="$(readlink -f $(dirname "${BASH_SOURCE[0]}")/..)"
|
|
USER_FILE="${UMBREL_ROOT}/db/user.json"
|
|
|
|
show_help() {
|
|
cat << EOF
|
|
app 0.0.1
|
|
|
|
CLI for managing Umbrel apps
|
|
|
|
Usage: app <command> <app> [<arguments>]
|
|
|
|
Commands:
|
|
install Pulls down images for an app and starts it
|
|
uninstall Removes images and destroys all data for an app
|
|
stop Stops an installed app
|
|
start Starts an installed app
|
|
compose Passes all arguments to docker-compose
|
|
ls-installed Lists installed apps
|
|
EOF
|
|
}
|
|
|
|
check_dependencies () {
|
|
for cmd in "$@"; do
|
|
if ! command -v $cmd >/dev/null 2>&1; then
|
|
echo "This script requires \"${cmd}\" to be installed"
|
|
exit 1
|
|
fi
|
|
done
|
|
}
|
|
|
|
list_installed_apps() {
|
|
cat "${USER_FILE}" 2> /dev/null | jq -r 'if has("installedApps") then .installedApps else [] end | join("\n")' || true
|
|
}
|
|
|
|
# Deterministically derives 128 bits of cryptographically secure entropy
|
|
derive_entropy () {
|
|
# Make sure we use the seed from the real Umbrel installation if this is
|
|
# an OTA update.
|
|
SEED_FILE="${UMBREL_ROOT}/db/umbrel-seed/seed"
|
|
if [[ ! -f "${SEED_FILE}" ]] && [[ -f "${UMBREL_ROOT}/../.umbrel" ]]; then
|
|
SEED_FILE="${UMBREL_ROOT}/../db/umbrel-seed/seed"
|
|
fi
|
|
|
|
identifier="${1}"
|
|
umbrel_seed=$(cat "${SEED_FILE}") || true
|
|
|
|
if [[ -z "$umbrel_seed" ]] || [[ -z "$identifier" ]]; then
|
|
>&2 echo "Missing derivation parameter, this is unsafe, exiting."
|
|
exit 1
|
|
fi
|
|
|
|
# We need `sed 's/^.* //'` to trim the "(stdin)= " prefix from some versions of openssl
|
|
printf "%s" "${identifier}" | openssl dgst -sha256 -hmac "${umbrel_seed}" | sed 's/^.* //'
|
|
}
|
|
|
|
# Check dependencies
|
|
check_dependencies docker-compose jq openssl
|
|
|
|
if [ -z ${1+x} ]; then
|
|
command=""
|
|
else
|
|
command="$1"
|
|
fi
|
|
|
|
# Lists installed apps
|
|
if [[ "$command" = "ls-installed" ]]; then
|
|
list_installed_apps
|
|
|
|
exit
|
|
fi
|
|
|
|
if [ -z ${2+x} ]; then
|
|
show_help
|
|
exit 1
|
|
else
|
|
app="$2"
|
|
app_dir="${UMBREL_ROOT}/apps/${app}"
|
|
app_data_dir="${UMBREL_ROOT}/app-data/${app}"
|
|
if [[ "${app}" == "installed" ]]; then
|
|
for app in $(list_installed_apps); do
|
|
if [[ "${app}" != "" ]]; then
|
|
"${0}" "${1}" "${app}" "${@:3}" &
|
|
fi
|
|
done
|
|
wait
|
|
exit
|
|
fi
|
|
if [[ -z "${app}" ]] || [[ ! -d "${app_dir}" ]]; then
|
|
echo "Error: \"${app}\" is not a valid app"
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
if [ -z ${3+x} ]; then
|
|
args=""
|
|
else
|
|
args="${@:3}"
|
|
fi
|
|
|
|
compose() {
|
|
local app="${1}"
|
|
shift
|
|
local env_file="${UMBREL_ROOT}/.env"
|
|
local app_base_compose_file="${UMBREL_ROOT}/apps/docker-compose.common.yml"
|
|
local app_compose_file="${app_dir}/docker-compose.yml"
|
|
local app_domain="$(hostname -s 2>/dev/null || echo "umbrel").local"
|
|
local app_hidden_servive_file="${UMBREL_ROOT}/tor/data/app-${app}/hostname"
|
|
local app_entropy_identifier="app-${app}-seed"
|
|
|
|
export BITCOIN_DATA_DIR="${UMBREL_ROOT}/bitcoin"
|
|
export LND_DATA_DIR="${UMBREL_ROOT}/lnd"
|
|
export APP_DATA_DIR="${app_data_dir}"
|
|
export APP_DOMAIN="${app_domain}"
|
|
export APP_HIDDEN_SERVICE="$(cat "${app_hidden_servive_file}" 2>/dev/null || echo "notyetset.onion")"
|
|
export APP_SEED=$(derive_entropy "${app_entropy_identifier}")
|
|
export APP_PASSWORD=$(derive_entropy "${app_entropy_identifier}-APP_PASSWORD")
|
|
|
|
# App specific env vars
|
|
# Note: Hardcoding app specific env vars is a short term solution. Long term
|
|
# these values will be registered in an apps manifest and generated dynamically.
|
|
local dojo_hidden_service_file="${UMBREL_ROOT}/tor/data/app-${app}-dojo/hostname"
|
|
local whirlpool_hidden_service_file="${UMBREL_ROOT}/tor/data/app-${app}-whirlpool/hostname"
|
|
local squeaknode_p2p_hidden_service_file="${UMBREL_ROOT}/tor/data/app-${app}-p2p/hostname"
|
|
export SAMOURAI_SERVER_DOJO_HIDDEN_SERVICE="$(cat "${dojo_hidden_service_file}" 2>/dev/null || echo "notyetset.onion")"
|
|
export SAMOURAI_SERVER_WHIRLPOOL_HIDDEN_SERVICE="$(cat "${whirlpool_hidden_service_file}" 2>/dev/null || echo "notyetset.onion")"
|
|
export SAMOURAI_SERVER_NODE_API_KEY=$(derive_entropy "env-${app_entropy_identifier}-NODE_API_KEY")
|
|
export SAMOURAI_SERVER_NODE_ADMIN_KEY=$(derive_entropy "env-${app_entropy_identifier}-NODE_ADMIN_KEY")
|
|
export SAMOURAI_SERVER_NODE_JWT_SECRET=$(derive_entropy "env-${app_entropy_identifier}-NODE_JWT_SECRET")
|
|
export SAMOURAI_SERVER_WHIRLPOOL_API_KEY=$(derive_entropy "env-${app_entropy_identifier}-WHIRLPOOL_API_KEY")
|
|
export SQUEAKNODE_P2P_HIDDEN_SERVICE="$(cat "${squeaknode_p2p_hidden_service_file}" 2>/dev/null || echo "notyetset.onion")"
|
|
|
|
docker-compose \
|
|
--env-file "${env_file}" \
|
|
--project-name "${app}" \
|
|
--file "${app_base_compose_file}" \
|
|
--file "${app_compose_file}" \
|
|
"${@}"
|
|
}
|
|
|
|
update_installed_apps() {
|
|
local action="${1}"
|
|
local app="${2}"
|
|
|
|
while ! (set -o noclobber; echo "$$" > "${USER_FILE}.lock") 2> /dev/null; do
|
|
echo "Waiting for JSON lock to be released for ${app} update..."
|
|
sleep 1
|
|
done
|
|
# This will cause the lock-file to be deleted in case of a
|
|
# premature exit.
|
|
trap "rm -f "${USER_FILE}.lock"; exit $?" INT TERM EXIT
|
|
|
|
[[ "${action}" == "add" ]] && operator="+" || operator="-"
|
|
updated_json=$(cat "${USER_FILE}" | jq ".installedApps |= (. ${operator} [\"${app}\"] | unique)")
|
|
echo "${updated_json}" > "${USER_FILE}"
|
|
|
|
rm -f "${USER_FILE}.lock"
|
|
}
|
|
|
|
# Pulls down images for an app and starts it
|
|
if [[ "$command" = "install" ]]; then
|
|
|
|
echo "Setting up data dir for app ${app}..."
|
|
mkdir -p "${app_data_dir}"
|
|
rsync --archive --verbose --exclude ".gitkeep" "${app_dir}/." "${app_data_dir}"
|
|
|
|
echo "Pulling images for app ${app}..."
|
|
compose "${app}" pull
|
|
|
|
echo "Starting app ${app}..."
|
|
compose "${app}" up --detach
|
|
|
|
echo "Saving app ${app} in DB..."
|
|
update_installed_apps add "${app}"
|
|
|
|
echo "Successfully installed app ${app}"
|
|
exit
|
|
fi
|
|
|
|
# Removes images and destroys all data for an app
|
|
if [[ "$command" = "uninstall" ]]; then
|
|
|
|
echo "Removing images for app ${app}..."
|
|
compose "${app}" down --rmi all --remove-orphans
|
|
|
|
echo "Deleting app data for app ${app}..."
|
|
if [[ -d "${app_data_dir}" ]]; then
|
|
rm -rf "${app_data_dir}"
|
|
fi
|
|
|
|
echo "Removing app ${app} from DB..."
|
|
update_installed_apps remove "${app}"
|
|
|
|
echo "Successfully uninstalled app ${app}"
|
|
exit
|
|
fi
|
|
|
|
# Stops an installed app
|
|
if [[ "$command" = "stop" ]]; then
|
|
|
|
echo "Stopping app ${app}..."
|
|
compose "${app}" rm --force --stop
|
|
|
|
exit
|
|
fi
|
|
|
|
# Starts an installed app
|
|
if [[ "$command" = "start" ]]; then
|
|
|
|
if ! list_installed_apps | grep --quiet "^${app}$"; then
|
|
echo "Error: app \"${app}\" is not installed yet"
|
|
exit 1
|
|
fi
|
|
|
|
echo "Starting app ${app}..."
|
|
compose "${app}" up --detach
|
|
|
|
exit
|
|
fi
|
|
|
|
# Passes all arguments to docker-compose
|
|
if [[ "$command" = "compose" ]]; then
|
|
compose "${app}" ${args}
|
|
|
|
exit
|
|
fi
|
|
|
|
# If we get here it means no valid command was supplied
|
|
# Show help and exit
|
|
show_help
|
|
exit 1
|
|
|