#!/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 [] 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