#!/usr/bin/env bash set -euo pipefail # This script is copied in from <1.0 Umbrel and modified to work with umbreld. # It provides a compatibility layer so the old app system works reliably. # TODO: Hardcoding these for now since they used to be read from .env # Figure out what other stuff we need from the old .env and how to pass it through. # GATEWAY_IP is also baked into the MCP connect UI as the endpoint URL that # agents installed as apps use to reach umbreld from inside their containers # (INSTALLED_AGENT_MCP_URL in ui/src/routes/settings/mcp/agents.ts) — keep # them in sync if this ever changes, e.g. when this script is refactored away. export NETWORK_IP='10.21.0.0' export GATEWAY_IP='10.21.0.1' VERSION="0.0.3" UMBREL_ROOT="${SCRIPT_UMBREL_ROOT}" USER_FILE="${UMBREL_ROOT}/db/user.json" CURRENT_SCRIPT_PATH="$(realpath "${BASH_SOURCE[0]}")" APP_PROXY_SERVICE_NAME="app_proxy" # We'll pass this in from umbreld # REMOTE_TOR_ACCESS="false" # if [[ -f "${USER_FILE}" ]]; then # REMOTE_TOR_ACCESS=$(cat "${USER_FILE}" | jq 'has("remoteTorAccess") and .remoteTorAccess') # fi show_help() { cat << EOF CLI (v${VERSION}) 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 reinstall Calls 'uninstall', followed by 'install' for an app start Starts an installed app stop Stops an installed app restart Restarts an installed app compose Passes all arguments to docker-compose ls-installed Lists installed apps ls-dependencies Lists dependencies of an app ls-transitive-dependencies Lists transitive dependencies of an app EOF } # Use GNU cp on macos if [[ "$(uname)" = "Darwin" ]]; then cp="gcp" else cp="cp" fi check_dependencies () { for cmd in "$@"; do if ! command -v $cmd >/dev/null 2>&1; then >&2 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 local yaml_file="${UMBREL_ROOT}/umbrel.yaml" yq e '.apps[]' "${yaml_file}" 2> /dev/null || true } list_dependencies_of() { local app="$1" local app_data_dir="${UMBREL_ROOT}/app-data/${app}" local app_manifest_file="${app_data_dir}/umbrel-app.yml" local app_settings_file="${app_data_dir}/settings.yml" # Get the app's dependencies and substitute with alternatives if present local dependencies=$(yq e '.dependencies[]' "${app_manifest_file}" 2> /dev/null || true) for dep in $dependencies; do yq e ".dependencies.${dep} // \"${dep}\"" "${app_settings_file}" 2> /dev/null || echo "${dep}" done } list_transitive_dependencies_of() { local app="$1" local -A processed=() local -A processing=() local output=() process_dependencies_in_post_order() { local current_app="$1" # Skip if already processed if [[ -n "${processed[$current_app]:-}" ]]; then return fi # Skip on circular dependencies if [[ -n "${processing[$current_app]:-}" ]]; then return 1 fi processing["$current_app"]=1 local deps=($(list_dependencies_of "$current_app")) # Process dependencies before processing the app itself for dep in "${deps[@]}"; do if ! process_dependencies_in_post_order "$dep"; then echo "Error: Circular dependency in app $app: $current_app -> $dep" >&2 processing["$current_app"]=0 return 1 fi done processed["$current_app"]=1 processing["$current_app"]=0 output+=("$current_app") } process_dependencies_in_post_order "$app" # Omit the app itself for ((i = 0; i < ${#output[@]} - 1; i++)); do echo "${output[i]}" done } # 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/^.* //' } # Define the stable app paths before any lifecycle hook runs. APP_DATA_DIR is # the fixed app definition directory; APP_DATA_ROOT is the app-owned persistent # data root and defaults to its data/ child until storage relocation is enabled. _umbrel_runtime_set_app_paths() { local -r app="${1}" local app_data_root app_data_root="$(printf '%s' "${SCRIPT_APP_DATA_ROOTS}" | jq --exit-status --raw-output --arg app "${app}" '.[$app]')" export APP_DATA_DIR="${UMBREL_ROOT}/app-data/${app}" export APP_DATA_ROOT="${app_data_root}" } # Setup env. for this context for a given app source_app() { local -r app="${1}" local -r _umbrel_runtime_data_roots="${SCRIPT_APP_DATA_ROOTS}" local _umbrel_runtime_app_data_root _umbrel_runtime_app_data_root="$(printf '%s' "${_umbrel_runtime_data_roots}" | jq --exit-status --raw-output --arg app "${app}" '.[$app]')" local -r app_domain="$(hostname -s 2>/dev/null || echo "umbrel").local" local -r app_entropy_identifier="app-${app}-seed" # Load in existing Umbrel .env # So that apps in their exports.sh can access # e.g. $TOR_PROXY_IP, $TOR_PROXY_PORT # [[ -f "${UMBREL_ROOT}/.env" ]] && . "${UMBREL_ROOT}/.env" export NETWORK_IP="${NETWORK_IP}" # Set other useful vars. used in exports export DEVICE_HOSTNAME="$(cat /proc/sys/kernel/hostname 2>/dev/null || echo "umbrel")" export DEVICE_DOMAIN_NAME="${DEVICE_HOSTNAME}.local" # Set env using all transitive dependencies exports.sh # Do this first so that no app exports can # Override any app specific exports defined below EXPORTS_TOR_DATA_DIR="${UMBREL_ROOT}/tor/data" APPS_TO_SOURCE="$(list_transitive_dependencies_of "$app")" # $app might not be in the 'installed apps list' yet # i.e. If it is currently being installed # So we'll add it to the list of apps that will be 'sourced' if ! echo "${APPS_TO_SOURCE}" | grep --quiet "^${app}$"; then APPS_TO_SOURCE="${APPS_TO_SOURCE}"$'\n'"${app}" fi for EXPORTS_APP_ID in $APPS_TO_SOURCE; do EXPORTS_APP_DIR="${UMBREL_ROOT}/app-data/${EXPORTS_APP_ID}" EXPORTS_APP_FILE="${EXPORTS_APP_DIR}/exports.sh" # Keep the existing cross-version export name while resolving it to the # app's current physical data root on supported umbrelOS versions. EXPORTS_APP_DATA_DIR="$(printf '%s' "${_umbrel_runtime_data_roots}" | jq --exit-status --raw-output --arg app "${EXPORTS_APP_ID}" '.[$app]')" if [[ -f "${EXPORTS_APP_FILE}" ]]; then # We replace the literal text "${UMBREL_ROOT}/scripts/app" within the exports.sh file with the path to this script sed -i 's|"${UMBREL_ROOT}/scripts/app"|'"${CURRENT_SCRIPT_PATH}"'|g' "${EXPORTS_APP_FILE}" # We replace the literal text "${UMBREL_ROOT}/db/user.json" within the exports.sh file with the path to the umbrel.yaml file # This specifically handles the Tailscale app sed -i 's|"${UMBREL_ROOT}/db/user.json"|"${UMBREL_ROOT}/umbrel.yaml"|g' "${EXPORTS_APP_FILE}" # Source the modified temporary exports file . "${EXPORTS_APP_FILE}" fi done # App specific exports # Re-assert these after sourcing dependency exports so an app cannot # accidentally override the runtime-owned path contract. export APP_DATA_DIR="${UMBREL_ROOT}/app-data/${app}" export APP_DATA_ROOT="${_umbrel_runtime_app_data_root}" export APP_ID="${app}" export APP_MANIFEST_FILE="${APP_DATA_DIR}/umbrel-app.yml" export APP_VERSION=$(cat "${APP_MANIFEST_FILE}" | yq '.version') # The manifest port remains the browser-facing port owned by umbreld's app gateway. export APP_PROXY_PORT=$(cat "${APP_MANIFEST_FILE}" | yq '.port') export APP_DOMAIN="${app_domain}" export APP_HIDDEN_SERVICE="not-enabled.onion" if [[ "${REMOTE_TOR_ACCESS}" == "true" ]]; then export APP_HIDDEN_SERVICE="$(cat "${app_hidden_service_file}" 2>/dev/null || echo "notyetset.onion")" fi export APP_SEED=$(derive_entropy "${app_entropy_identifier}") export APP_PASSWORD=$(derive_entropy "${app_entropy_identifier}-APP_PASSWORD") # Tor specific exports export TOR_DATA_DIR="${UMBREL_ROOT}/tor/data" export TOR_ENTRYPOINT_SCRIPT="${SCRIPT_DOCKER_FRAGMENTS}/tor-entrypoint.sh" export TOR_HS_APP_DIR="/data/app-${app}" export TOR_HS_PORTS="80:${GATEWAY_IP}:${APP_PROXY_PORT}" # TODO: Look into why this needed to be commented out for Mark. # tor_extra_hs_varname=$(echo "APP_${APP_ID^^}_TOR_HS_EXTRA_PORTS" | tr '-' '_') # tor_hs_extra_ports="${!tor_extra_hs_varname:-}" # if [[ ! -z "${tor_hs_extra_ports}" ]]; then # export TOR_HS_PORTS="${TOR_HS_PORTS} ${tor_hs_extra_ports}" # fi # Other export UMBREL_ROOT } # Check dependencies check_dependencies docker jq yq openssl envsubst 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" # Lists (transitive) dependencies if [[ "$command" = "ls-dependencies" ]]; then list_dependencies_of "$app" exit elif [[ "$command" = "ls-transitive-dependencies" ]]; then list_transitive_dependencies_of "$app" exit fi # repo=$(cat "${USER_FILE}" 2> /dev/null | jq -r ".appOrigin.\"${app}\"" || true) # repo_path=$("${UMBREL_ROOT}/scripts/repo" "path" "${repo}") # app_repo_dir="${repo_path}/${app}" app_repo_dir="${SCRIPT_APP_REPO_DIR}" app_data_dir="${UMBREL_ROOT}/app-data/${app}" app_hidden_service_file="${UMBREL_ROOT}/tor/data/app-${app}/hostname" 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}" ]]; then >&2 echo "Error: \"${app}\" is not a valid app" exit 1 fi fi # Hooks such as pre-install, pre-stop, pre-update, and pre-uninstall run before # source_app in their command paths, so establish the app data contract here. _umbrel_runtime_set_app_paths "${app}" if [ -z ${3+x} ]; then args="" else args="${@:3}" fi execute_hook() { local -r app="${1}" local -r name="${2}" local -r app_hooks_dir="${UMBREL_ROOT}/app-data/${app}/hooks" local -r hook="${app_hooks_dir}/${name}" if [[ -x "${hook}" ]]; then echo "Executing hook: ${hook}" # We replace the literal text "${UMBREL_ROOT}/scripts/app" within the hook file with the path to this script sed -i 's|"${UMBREL_ROOT}/scripts/app"|'"${CURRENT_SCRIPT_PATH}"'|g' "${hook}" # Swallow non-zero exit code "${hook}" || true fi } compose() { local -r app="${1}" shift # Source env. source_app "${app}" # Define support compose files local -r app_gateway_compose_file="${SCRIPT_DOCKER_FRAGMENTS}/docker-compose.app_gateway.yml" local -r tor_compose_file="${SCRIPT_DOCKER_FRAGMENTS}/docker-compose.tor.yml" local -r common_compose_file="${SCRIPT_DOCKER_FRAGMENTS}/docker-compose.common.yml" local -r app_compose_file="${app_data_dir}/docker-compose.yml" local -r user_settings_compose_file="${app_data_dir}/docker-compose.umbrel-user-settings.yml" local app_runtime_compose_file="${app_compose_file}" local -r umbrel_env_file="${UMBREL_ROOT}/.env" compose_files=() # Detect if the 'app_proxy' service has been defined # In the app's docker-compose file has_app_proxy_service=$(cat "${app_compose_file}" | yq ".services | has(\"${APP_PROXY_SERVICE_NAME}\")") if [[ "${has_app_proxy_service}" == "true" ]]; then # Render app_proxy configuration with the exact environment Docker Compose # uses for the app. Umbreld consumes this file for its in-process gateway; # the app_proxy service itself is still removed from the runtime compose. local app_gateway_config_file="${app_data_dir}/app-gateway.json" local app_gateway_config_temp_file="${app_gateway_config_file}.tmp.$$" if ! docker compose \ --project-name "${app}" \ --file "${app_gateway_compose_file}" \ --file "${app_compose_file}" \ config --format json \ | jq '(.services.app_proxy.environment // {}) | { APP_HOST: .APP_HOST, APP_PORT: .APP_PORT, APP_PROTOCOL: .APP_PROTOCOL, PROXY_AUTH_ADD: .PROXY_AUTH_ADD, PROXY_AUTH_WHITELIST: .PROXY_AUTH_WHITELIST, PROXY_AUTH_BLACKLIST: .PROXY_AUTH_BLACKLIST, PROXY_TRUST_UPSTREAM: .PROXY_TRUST_UPSTREAM, PROXY_TIMEOUT: .PROXY_TIMEOUT } | with_entries(select(.value != null))' > "${app_gateway_config_temp_file}" then rm -f "${app_gateway_config_temp_file}" return 1 fi mv "${app_gateway_config_temp_file}" "${app_gateway_config_file}" chmod 600 "${app_gateway_config_file}" # app_proxy is now implemented in umbreld. Keep its declaration in the # installed compose file as gateway configuration, but never create a sidecar. app_runtime_compose_file="${app_data_dir}/docker-compose.umbreld.yml" yq 'del(.services.app_proxy)' "${app_compose_file}" > "${app_runtime_compose_file}" else rm -f "${app_data_dir}/app-gateway.json" fi # If remote Tor access is enabled and the app has an app_proxy service # then include a compose file for Tor. The hidden service points at the # host app gateway, so apps without one (e.g. host network apps # like Tailscale) must not get a tor_server sidecar, it would crash loop # trying to resolve a hostname that never exists. if [[ "${REMOTE_TOR_ACCESS}" == "true" ]] && [[ "${has_app_proxy_service}" == "true" ]]; then compose_files+=( "--file" "${tor_compose_file}" ) fi # Add app's compose file last so that it can override # Any of the other compose files compose_files+=( "--file" "${common_compose_file}" ) compose_files+=( "--file" "${app_runtime_compose_file}" ) if [[ -f "${user_settings_compose_file}" ]]; then compose_files+=( "--file" "${user_settings_compose_file}" ) fi # Merge compose files and args. passed into 'compose' compose_args=("${compose_files[@]}" "${@}") # TODO: We removed the .env source, we probs need to add that back in somehow # --env-file "${umbrel_env_file}" \ docker compose \ --project-name "${app}" \ "${compose_args[@]}" } update_installed_apps() { local -r action="${1}" local -r app="${2}" local -r repo="${3:-null}" 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}" if [[ "${action}" == "add" ]]; then updated_json=$(cat "${USER_FILE}" | jq ".appOrigin |= (. ${operator} {\"${app}\":\"${repo}\"})") else updated_json=$(cat "${USER_FILE}" | jq "del(.appOrigin.\"${app}\")") fi echo "${updated_json}" > "${USER_FILE}" rm -f "${USER_FILE}.lock" } template_app() { local -r app="${1}" # Loop over all templates within app and populate them APP_TEMPLATE_FILES="${app_data_dir}/*.template" shopt -s nullglob for APP_TEMPLATE_INPUT_FILE in $APP_TEMPLATE_FILES; do # Output filename is the same as input with .template stripped off APP_TEMPLATE_OUTPUT_FILE="${APP_TEMPLATE_INPUT_FILE%.*}" # First we'll copy the file so we ensure the output # has the same fs permissions as the input $cp --archive "${APP_TEMPLATE_INPUT_FILE}" "${APP_TEMPLATE_OUTPUT_FILE}" cat "${APP_TEMPLATE_INPUT_FILE}" | envsubst > "${APP_TEMPLATE_OUTPUT_FILE}" done } copy_app_files() { local -r files_to_copy="${1}" for filename in $files_to_copy; do APP_FILES="${app_repo_dir}/${filename}" for app_file in $APP_FILES; do if [[ -f "${app_file}" ]] || [[ -d "${app_file}" ]]; then $cp --archive "${app_file}" "${app_data_dir}" fi done done } wait_for_tor_hs() { local -r app="${1}" # Check if the app's hidden service hostname # Has been already generated and exit early if [[ -f "${app_hidden_service_file}" ]]; then return fi # Check that the app has the App Proxy service defined local -r app_compose_file="${app_data_dir}/docker-compose.yml" has_app_proxy_service=$(cat "${app_compose_file}" | yq ".services | has(\"${APP_PROXY_SERVICE_NAME}\")") if [[ "${has_app_proxy_service}" == "false" ]]; then echo >&2 echo "Warning: \"${app}\" has no '${APP_PROXY_SERVICE_NAME}' defined" >&2 echo " \"${app}\" needs this to generate Tor HS" echo return fi # If a tor service will start # and there is no existing tor hs hostname # Let's allow 10 seconds to generate it and then start the app if [[ "${REMOTE_TOR_ACCESS}" == "true" ]]; then echo "Generating hidden services for ${app}..." compose "${app}" up --detach tor_server for attempt in $(seq 1 100); do if [[ -f "${app_hidden_service_file}" ]]; then echo "Hidden service file created successfully!" break fi sleep 0.1 done if [[ ! -f "${app_hidden_service_file}" ]]; then echo "Hidden service file wasn't created" fi fi } start_app() { local -r app="${1}" # Source env. source_app "${app}" # Now apply templates template_app "${app}" # Wait for Tor's HS hostname to exist wait_for_tor_hs "${app}" execute_hook "${app}" "pre-start" # Start all the app's containers compose "${app}" up --detach --build execute_hook "${app}" "post-start" } # Check that the app is installed must_be_installed_guard() { if ! list_installed_apps | grep --quiet "^${app}$"; then >&2 echo "Error: app \"${app}\" is not installed yet" exit 1 fi } # Recreates install-time state after the user abandons a missing data root. # Existing images are retained, so recovery does not require another download. if [[ "$command" = "initialize-data-root" ]]; then execute_hook "${app}" "pre-install" start_app "${app}" execute_hook "${app}" "post-install" exit fi # Pulls down images for an app and starts it if [[ "$command" = "install" ]]; then # repo=$("${UMBREL_ROOT}/scripts/repo" "locate" "${app}") # if [[ -z "${repo}" ]]; then # >&2 echo "Error: \"${app}\" not found in any local app repo" # exit 1 # fi # app_repo_dir=$("${UMBREL_ROOT}/scripts/repo" "path" "${repo}") # app_repo_dir="${app_repo_dir}/${app}" # echo "Installing '${app}' from: ${repo}" # echo "Setting up data dir for app ${app}..." # mkdir -p "${app_data_dir}" # # Copy all app files # rsync --archive --verbose --exclude ".gitkeep" "${app_repo_dir}/." "${app_data_dir}" execute_hook "${app}" "pre-install" # Source env. source_app "${app}" # Now apply templates template_app "${app}" echo "Pulling images for app ${app}..." compose "${app}" pull # if [[ "$*" != *"--skip-start"* ]]; then # echo "Starting app ${app}..." start_app "${app}" # fi # echo "Saving app ${app} in DB..." # update_installed_apps add "${app}" "${repo}" execute_hook "${app}" "post-install" echo "Successfully installed app ${app}" exit fi # Removes images and destroys all data for an app if [[ "$command" = "uninstall" ]]; then must_be_installed_guard execute_hook "${app}" "pre-uninstall" # If a post uninstal hook exists # Then make a copy before it's deleted below app_hooks_dir="${UMBREL_ROOT}/app-data/${app}/hooks" post_uninstall_app_hook="${app_hooks_dir}/post-uninstall" if [[ -x "${post_uninstall_app_hook}" ]]; then temp_post_uninstall_app_hook="/tmp/${app}-post-uninstall" $cp --archive "${post_uninstall_app_hook}" "${temp_post_uninstall_app_hook}" post_uninstall_app_hook="${temp_post_uninstall_app_hook}" else post_uninstall_app_hook="" fi 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}" if [[ ! -z "${post_uninstall_app_hook}" ]]; then "${post_uninstall_app_hook}" || true rm -rf "${post_uninstall_app_hook}" fi echo "Successfully uninstalled app ${app}" exit fi # Stops an installed app if [[ "$command" = "stop" ]]; then # must_be_installed_guard execute_hook "${app}" "pre-stop" echo "Stopping app ${app}..." compose "${app}" rm --force --stop execute_hook "${app}" "post-stop" exit fi # Removes app containers without running hooks. This is reserved for recovery # when the app's configured storage is unavailable and its hooks cannot run. if [[ "$command" = "force-stop" ]]; then compose "${app}" down --remove-orphans exit fi if [[ "$command" = "reinstall" ]]; then "${0}" "uninstall" "${app}" echo "${0}" "install" "${app}" exit fi # Starts an installed app if [[ "$command" = "start" ]]; then # must_be_installed_guard echo "Starting app ${app}..." start_app "${app}" exit fi # Restarts an installed app if [[ "$command" = "restart" ]]; then "${0}" "stop" "${app}" "${0}" "start" "${app}" exit fi # Get logs for an app if [[ "$command" = "logs" ]]; then if [[ -n "${3:-}" ]]; then compose "${app}" logs --tail 500 | tail --bytes "${3}" else compose "${app}" logs --tail 500 fi exit fi # Update an installed app if [[ "$command" = "update" ]]; then # must_be_installed_guard # Check that the app folder still exists # Within the associated local app repo if [[ ! -d "${app_repo_dir}" ]]; then >&2 echo "Error: Local app repo no longer exists for ${app}" exit 1 fi echo "Updating '${app}' from: ${app_repo_dir}" # Save current images to clean up later app_compose_file="${app_data_dir}/docker-compose.yml" app_old_images=$(yq e '.services | map(select(.image != null)) | .[].image' "${app_compose_file}") if [[ "$*" != *"--skip-stop"* ]]; then "${0}" "stop" "${app}" fi execute_hook "${app}" "pre-update" # App updates will only copy files from this whitelist: UPDATE_FILES_WHITELIST_PRE="docker-compose.yml *.template exports.sh torrc hooks" # We copy umbrel-app.yml after the app has started # That way the frontend knows the update has finished # And the app is running again UPDATE_FILES_WHITELIST_POST="umbrel-app.yml" copy_app_files "${UPDATE_FILES_WHITELIST_PRE}" # Ensure remaining files are copied in case of unexpected exit trap "copy_app_files "${UPDATE_FILES_WHITELIST_POST}"; exit $?" INT TERM EXIT # Source env. after new exports.sh is copied (done above via 'copy_app_files') source_app "${app}" # Now apply templates template_app "${app}" echo "Pulling images for app ${app}..." compose "${app}" pull # Copy remaining files to mark update as complete copy_app_files "${UPDATE_FILES_WHITELIST_POST}" if [[ "$*" != *"--skip-start"* ]]; then "${0}" "start" "${app}" # Remove any old images we don't need anymore docker rmi $app_old_images || true fi execute_hook "${app}" "post-update" exit fi # Update an installed app if [[ "$command" = "pre-patch-update" ]]; then # must_be_installed_guard # Check that the app folder still exists # Within the associated local app repo if [[ ! -d "${app_repo_dir}" ]]; then >&2 echo "Error: Local app repo no longer exists for ${app}" exit 1 fi echo "Updating '${app}' from: ${app_repo_dir}" # Save current images to clean up later app_compose_file="${app_data_dir}/docker-compose.yml" app_old_images=$(yq e '.services | map(select(.image != null)) | .[].image' "${app_compose_file}") if [[ "$*" != *"--skip-stop"* ]]; then "${0}" "stop" "${app}" fi execute_hook "${app}" "pre-update" # App updates will only copy files from this whitelist: UPDATE_FILES_WHITELIST_PRE="docker-compose.yml *.template exports.sh torrc hooks" # We copy umbrel-app.yml after the app has started # That way the frontend knows the update has finished # And the app is running again UPDATE_FILES_WHITELIST_POST="umbrel-app.yml" copy_app_files "${UPDATE_FILES_WHITELIST_PRE}" # Ensure remaining files are copied in case of unexpected exit trap "copy_app_files "${UPDATE_FILES_WHITELIST_POST}"; exit $?" INT TERM EXIT # Source env. after new exports.sh is copied (done above via 'copy_app_files') source_app "${app}" # Now apply templates template_app "${app}" # Copy remaining files to mark update as complete copy_app_files "${UPDATE_FILES_WHITELIST_POST}" exit fi # Update an installed app if [[ "$command" = "post-patch-update" ]]; then # umbreld pulls and starts the app between the split update phases. Keeping # startup in TypeScript lets normal starts and fresh-data initialization use # the same recovery path. execute_hook "${app}" "post-update" exit fi # Nuke app images if [[ "$command" = "nuke-images" ]]; then compose "${app}" down --rmi all --remove-orphans 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