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.
 
 

224 lines
6.1 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
list_installed_apps | while read app; do
if [[ "${app}" != "" ]]; then
"${0}" "${1}" "${app}" "${@:3}" || true
fi
done
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_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_HIDDEN_SERVICE="$(cat "${app_hidden_servive_file}" 2>/dev/null || echo "notyetset.onion")"
export APP_SEED=$(derive_entropy "${app_entropy_identifier}")
# 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 whirlpool_hidden_service_file="${UMBREL_ROOT}/tor/data/app-${app}-whirlpool/hostname"
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")
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