diff --git a/scripts/easy-docker/lib/app/wizard/common/site.sh b/scripts/easy-docker/lib/app/wizard/common/site.sh index 12a81680..098140cd 100755 --- a/scripts/easy-docker/lib/app/wizard/common/site.sh +++ b/scripts/easy-docker/lib/app/wizard/common/site.sh @@ -1,5 +1,8 @@ #!/usr/bin/env bash +EASY_DOCKER_SITE_ERROR_DETAIL="" +EASY_DOCKER_SITE_ERROR_LOG_PATH="" + load_easy_docker_site_modules() { local site_dir="" site_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/site" diff --git a/scripts/easy-docker/lib/app/wizard/common/site/bootstrap.sh b/scripts/easy-docker/lib/app/wizard/common/site/bootstrap.sh index 54383f61..3b5fd2ae 100755 --- a/scripts/easy-docker/lib/app/wizard/common/site/bootstrap.sh +++ b/scripts/easy-docker/lib/app/wizard/common/site/bootstrap.sh @@ -190,11 +190,98 @@ EOF wrapped_backend_command="$(printf "cd /home/frappe/frappe-bench && %s" "${backend_command}")" if [ -n "${fallback_erpnext_version}" ]; then - ERPNEXT_VERSION="${fallback_erpnext_version}" docker compose --project-name "${compose_project_name}" --env-file "${env_path}" "${compose_args[@]}" exec -T backend bash -lc "${wrapped_backend_command}" + ERPNEXT_VERSION="${fallback_erpnext_version}" docker compose --project-name "${compose_project_name}" --env-file "${env_path}" "${compose_args[@]}" exec -T backend bash -lc "${wrapped_backend_command}" "${absolute_path}"; then + return 1 + fi + + printf -v "${result_var}" "%s" "${relative_path}" + return 0 +} + +run_stack_backend_bash_command_capture() { + local result_var="${1}" + local stack_dir="${2}" + local backend_command="${3}" + local command_output="" + local command_status=0 + + reset_easy_docker_site_error_state + command_output="$(run_stack_backend_bash_command "${stack_dir}" "${backend_command}" 2>&1)" + command_status=$? + + if [ -n "${command_output}" ]; then + printf '%s\n' "${command_output}" + fi + + printf -v "${result_var}" "%s" "${command_output}" + return "${command_status}" +} + +capture_stack_site_error_log() { + local stack_dir="${1}" + local action_name="${2:-site-error}" + local error_output="${3:-}" + local log_path="" + + EASY_DOCKER_SITE_ERROR_LOG_PATH="" + if [ -z "${error_output}" ]; then + return 0 + fi + + if ! write_stack_site_error_log log_path "${stack_dir}" "${action_name}" "${error_output}"; then + EASY_DOCKER_SITE_ERROR_DETAIL="${EASY_DOCKER_SITE_ERROR_DETAIL:-Failed to write site error log.}" + return 1 + fi + + EASY_DOCKER_SITE_ERROR_LOG_PATH="${log_path}" + return 0 } stack_backend_service_is_running() { @@ -359,6 +446,76 @@ EOF run_stack_backend_bash_command "${stack_dir}" "${read_command}" } +get_stack_site_runtime_app_names_lines() { + local stack_dir="${1}" + local site_name="${2}" + local list_apps_command="" + + if ! is_safe_stack_site_cleanup_name "${site_name}"; then + return 61 + fi + + list_apps_command="$( + printf "bench --site %s list-apps | awk 'NF { print \$1 }'" \ + "$(shell_quote_site_command_arg "${site_name}")" + )" + + run_stack_backend_bash_command "${stack_dir}" "${list_apps_command}" +} + +get_stack_runtime_available_app_lines() { + local stack_dir="${1}" + + run_stack_backend_bash_command "${stack_dir}" "ls -1 apps" +} + +get_stack_site_runtime_selected_apps_lines() { + local result_var="${1}" + local stack_dir="${2}" + local site_name="${3}" + local selected_app_lines="" + local runtime_app_lines="" + local selected_app_name="" + local installed_app_lines="" + + if ! get_stack_selected_installable_apps selected_app_lines "${stack_dir}"; then + printf -v "${result_var}" "%s" "" + return 0 + fi + + if [ -z "${selected_app_lines}" ]; then + printf -v "${result_var}" "%s" "" + return 0 + fi + + runtime_app_lines="$(get_stack_site_runtime_app_names_lines "${stack_dir}" "${site_name}" || true)" + if [ -z "${runtime_app_lines}" ]; then + printf -v "${result_var}" "%s" "" + return 1 + fi + + while IFS= read -r selected_app_name; do + if [ -z "${selected_app_name}" ]; then + continue + fi + + if ! printf '%s\n' "${runtime_app_lines}" | grep -F -x -- "${selected_app_name}" >/dev/null 2>&1; then + continue + fi + + if [ -z "${installed_app_lines}" ]; then + installed_app_lines="${selected_app_name}" + else + installed_app_lines="${installed_app_lines}"$'\n'"${selected_app_name}" + fi + done </dev/null 2>&1 || true return 55 fi @@ -612,24 +772,52 @@ install_stack_apps_on_site() { local installed_app_lines="" local app_name="" local install_app_command="" + local install_app_output="" + local available_app_lines="" + local -a selected_apps=() if ! get_stack_selected_installable_apps selected_app_lines "${stack_dir}"; then printf -v "${result_var}" "%s" "" return 0 fi - while IFS= read -r app_name; do + if [ -z "${selected_app_lines}" ]; then + printf -v "${result_var}" "%s" "" + return 0 + fi + + available_app_lines="$(get_stack_runtime_available_app_lines "${stack_dir}" || true)" + if [ -z "${available_app_lines}" ]; then + EASY_DOCKER_SITE_ERROR_DETAIL="Could not inspect available apps in the backend image." + capture_stack_site_error_log "${stack_dir}" "site-install-apps-error" "easy-docker could not list /home/frappe/frappe-bench/apps before install-app." >/dev/null 2>&1 || true + return 63 + fi + + mapfile -t selected_apps <<<"${selected_app_lines}" + for app_name in "${selected_apps[@]}"; do if [ -z "${app_name}" ]; then continue fi + if ! printf '%s\n' "${available_app_lines}" | grep -F -x -- "${app_name}" >/dev/null 2>&1; then + EASY_DOCKER_SITE_ERROR_DETAIL="$(printf "Selected app '%s' is not available in the backend image." "${app_name}")" + capture_stack_site_error_log "${stack_dir}" "site-install-apps-error" "$(printf "Selected app '%s' was requested in stack metadata but is missing from /home/frappe/frappe-bench/apps.\nAvailable apps:\n%s" "${app_name}" "${available_app_lines}")" >/dev/null 2>&1 || true + if [ -n "${EASY_DOCKER_SITE_ERROR_LOG_PATH}" ]; then + printf 'Details written to %s\n' "${stack_dir}/${EASY_DOCKER_SITE_ERROR_LOG_PATH}" >&2 + fi + printf -v "${result_var}" "%s" "${installed_app_lines}" + return 63 + fi + install_app_command="$( printf "bench --site %s install-app %s" \ "$(shell_quote_site_command_arg "${site_name}")" \ "$(shell_quote_site_command_arg "${app_name}")" )" - if ! run_stack_backend_bash_command "${stack_dir}" "${install_app_command}"; then + if ! run_stack_backend_bash_command_capture install_app_output "${stack_dir}" "${install_app_command}"; then + EASY_DOCKER_SITE_ERROR_DETAIL="$(printf "bench install-app failed for '%s'." "${app_name}")" + capture_stack_site_error_log "${stack_dir}" "site-install-apps-error" "${install_app_output}" >/dev/null 2>&1 || true printf -v "${result_var}" "%s" "${installed_app_lines}" return 56 fi @@ -648,13 +836,12 @@ install_stack_apps_on_site() { "${installed_app_lines}" \ "install-apps" \ "" \ + "" \ "$(get_stack_site_created_at "${stack_dir}" || true)" \ "$(get_current_utc_timestamp)"; then return 58 fi - done </dev/null 2>&1 || true + mark_stack_site_failed "${stack_dir}" "${site_name}" "" "cleanup-partial-site" "Partial site artifacts could not be removed automatically. Manual cleanup is required." "" "" >/dev/null 2>&1 || true return 60 ;; *) - mark_stack_site_failed "${stack_dir}" "${site_name}" "" "cleanup-partial-site" "Unexpected cleanup failure before create-site." "${created_at}" >/dev/null 2>&1 || true + mark_stack_site_failed "${stack_dir}" "${site_name}" "" "cleanup-partial-site" "Unexpected cleanup failure before create-site." "" "${created_at}" >/dev/null 2>&1 || true return 60 ;; esac fi updated_at="${created_at}" - if ! persist_stack_site_metadata "${stack_dir}" "single-site" "${site_name}" "creating" "" "create-site" "" "${created_at}" "${updated_at}"; then + if ! persist_stack_site_metadata "${stack_dir}" "single-site" "${site_name}" "creating" "" "create-site" "" "" "${created_at}" "${updated_at}"; then return 58 fi @@ -734,12 +921,12 @@ bootstrap_first_stack_site() { else site_create_status=$? if cleanup_partial_stack_site "${stack_dir}" "${site_name}"; then - mark_stack_site_failed "${stack_dir}" "${site_name}" "" "create-site" "bench new-site failed. Partial site data was cleaned up automatically." "${created_at}" >/dev/null 2>&1 || true + mark_stack_site_failed "${stack_dir}" "${site_name}" "" "create-site" "bench new-site failed. Partial site data was cleaned up automatically." "${EASY_DOCKER_SITE_ERROR_LOG_PATH}" "${created_at}" >/dev/null 2>&1 || true return "${site_create_status}" fi cleanup_status=$? - mark_stack_site_failed "${stack_dir}" "${site_name}" "" "create-site" "bench new-site failed and partial site data could not be cleaned up automatically. Manual cleanup is required." "${created_at}" >/dev/null 2>&1 || true + mark_stack_site_failed "${stack_dir}" "${site_name}" "" "create-site" "bench new-site failed and partial site data could not be cleaned up automatically. Manual cleanup is required." "${EASY_DOCKER_SITE_ERROR_LOG_PATH}" "${created_at}" >/dev/null 2>&1 || true case "${cleanup_status}" in 54 | 52) return "${cleanup_status}" @@ -751,7 +938,7 @@ bootstrap_first_stack_site() { fi updated_at="$(get_current_utc_timestamp)" - if ! persist_stack_site_metadata "${stack_dir}" "single-site" "${site_name}" "created" "" "create-site" "" "${created_at}" "${updated_at}"; then + if ! persist_stack_site_metadata "${stack_dir}" "single-site" "${site_name}" "created" "" "create-site" "" "" "${created_at}" "${updated_at}"; then return 58 fi @@ -760,12 +947,12 @@ bootstrap_first_stack_site() { else app_install_status=$? case "${app_install_status}" in - 56) + 56 | 63) if cleanup_partial_stack_site "${stack_dir}" "${site_name}"; then - mark_stack_site_failed "${stack_dir}" "${site_name}" "${installed_app_lines}" "install-apps" "App installation failed. Partial site data was cleaned up automatically." "${created_at}" >/dev/null 2>&1 || true + mark_stack_site_failed "${stack_dir}" "${site_name}" "${installed_app_lines}" "install-apps" "${EASY_DOCKER_SITE_ERROR_DETAIL:-App installation failed. Partial site data was cleaned up automatically.}" "${EASY_DOCKER_SITE_ERROR_LOG_PATH}" "${created_at}" >/dev/null 2>&1 || true else cleanup_status=$? - mark_stack_site_failed "${stack_dir}" "${site_name}" "${installed_app_lines}" "install-apps" "App installation failed and partial site data could not be cleaned up automatically. Manual cleanup is required." "${created_at}" >/dev/null 2>&1 || true + mark_stack_site_failed "${stack_dir}" "${site_name}" "${installed_app_lines}" "install-apps" "${EASY_DOCKER_SITE_ERROR_DETAIL:-App installation failed and partial site data could not be cleaned up automatically. Manual cleanup is required.}" "${EASY_DOCKER_SITE_ERROR_LOG_PATH}" "${created_at}" >/dev/null 2>&1 || true case "${cleanup_status}" in 54 | 52) return "${cleanup_status}" @@ -780,14 +967,14 @@ bootstrap_first_stack_site() { return 58 ;; *) - mark_stack_site_failed "${stack_dir}" "${site_name}" "${installed_app_lines}" "install-apps" "Unknown app installation failure." "${created_at}" >/dev/null 2>&1 || true + mark_stack_site_failed "${stack_dir}" "${site_name}" "${installed_app_lines}" "install-apps" "Unknown app installation failure." "${EASY_DOCKER_SITE_ERROR_LOG_PATH}" "${created_at}" >/dev/null 2>&1 || true ;; esac return "${app_install_status}" fi updated_at="$(get_current_utc_timestamp)" - if ! persist_stack_site_metadata "${stack_dir}" "single-site" "${site_name}" "ready" "${installed_app_lines}" "install-apps" "" "${created_at}" "${updated_at}"; then + if ! persist_stack_site_metadata "${stack_dir}" "single-site" "${site_name}" "ready" "${installed_app_lines}" "install-apps" "" "" "${created_at}" "${updated_at}"; then return 58 fi diff --git a/scripts/easy-docker/lib/app/wizard/common/site/metadata.sh b/scripts/easy-docker/lib/app/wizard/common/site/metadata.sh index e20e39d9..49a7a187 100755 --- a/scripts/easy-docker/lib/app/wizard/common/site/metadata.sh +++ b/scripts/easy-docker/lib/app/wizard/common/site/metadata.sh @@ -220,19 +220,21 @@ build_stack_site_metadata_json_object() { local apps_installed_lines="${5:-}" local last_action="${6:-}" local last_error="${7:-}" - local created_at="${8:-}" - local updated_at="${9:-}" + local error_log_path="${8:-}" + local created_at="${9:-}" + local updated_at="${10:-}" local apps_installed_json_array="" build_stack_site_apps_installed_json_array apps_installed_json_array "${apps_installed_lines}" - printf -v "${result_var}" '{\n "mode": "%s",\n "name": "%s",\n "state": "%s",\n "apps_installed": %s,\n "last_action": "%s",\n "last_error": "%s",\n "created_at": "%s",\n "updated_at": "%s"\n }' \ + printf -v "${result_var}" '{\n "mode": "%s",\n "name": "%s",\n "state": "%s",\n "apps_installed": %s,\n "last_action": "%s",\n "last_error": "%s",\n "error_log_path": "%s",\n "created_at": "%s",\n "updated_at": "%s"\n }' \ "$(json_escape_string "${site_mode}")" \ "$(json_escape_string "${site_name}")" \ "$(json_escape_string "${site_state}")" \ "${apps_installed_json_array}" \ "$(json_escape_string "${last_action}")" \ "$(json_escape_string "${last_error}")" \ + "$(json_escape_string "${error_log_path}")" \ "$(json_escape_string "${created_at}")" \ "$(json_escape_string "${updated_at}")" } @@ -245,8 +247,9 @@ persist_stack_site_metadata() { local apps_installed_lines="${5:-}" local last_action="${6:-}" local last_error="${7:-}" - local created_at="${8:-}" - local updated_at="${9:-}" + local error_log_path="${8:-}" + local created_at="${9:-}" + local updated_at="${10:-}" local metadata_path="" local metadata_tmp_path="" local site_json_object="" @@ -257,7 +260,7 @@ persist_stack_site_metadata() { return 1 fi - build_stack_site_metadata_json_object site_json_object "${site_mode}" "${site_name}" "${site_state}" "${apps_installed_lines}" "${last_action}" "${last_error}" "${created_at}" "${updated_at}" + build_stack_site_metadata_json_object site_json_object "${site_mode}" "${site_name}" "${site_state}" "${apps_installed_lines}" "${last_action}" "${last_error}" "${error_log_path}" "${created_at}" "${updated_at}" if ! awk -v site_object="${site_json_object}" ' BEGIN { @@ -342,9 +345,10 @@ mark_stack_site_failed() { local apps_installed_lines="${3:-}" local last_action="${4:-bootstrap-site}" local last_error="${5:-Unknown site bootstrap failure}" - local created_at="${6:-}" + local error_log_path="${6:-}" + local created_at="${7:-}" local updated_at="" updated_at="$(get_current_utc_timestamp)" - persist_stack_site_metadata "${stack_dir}" "single-site" "${site_name}" "failed" "${apps_installed_lines}" "${last_action}" "${last_error}" "${created_at}" "${updated_at}" + persist_stack_site_metadata "${stack_dir}" "single-site" "${site_name}" "failed" "${apps_installed_lines}" "${last_action}" "${last_error}" "${error_log_path}" "${created_at}" "${updated_at}" } diff --git a/scripts/easy-docker/lib/app/wizard/flows/manage.sh b/scripts/easy-docker/lib/app/wizard/flows/manage.sh index 7e019813..7d4c3063 100755 --- a/scripts/easy-docker/lib/app/wizard/flows/manage.sh +++ b/scripts/easy-docker/lib/app/wizard/flows/manage.sh @@ -131,7 +131,6 @@ prompt_manage_stack_site_admin_password_with_cancel() { handle_manage_stack_site_flow() { local stack_name="${1}" local stack_dir="${2}" - local site_status_label="" local site_action="" local site_name="" local admin_password="" @@ -144,11 +143,10 @@ handle_manage_stack_site_flow() { local existing_site_details_action="" while true; do - get_stack_site_status_label site_status_label "${stack_dir}" existing_site_entry="" get_stack_site_menu_entry existing_site_entry "${stack_dir}" || true - site_action="$(show_manage_stack_site_menu "${stack_name}" "${stack_dir}" "${site_status_label}" "${existing_site_entry}" || true)" + site_action="$(show_manage_stack_site_menu "${stack_name}" "${stack_dir}" "${existing_site_entry}" || true)" case "${site_action}" in "Create new site") if ! prompt_manage_stack_site_name_with_cancel site_name "${stack_name}" "${stack_dir}"; then @@ -181,10 +179,10 @@ handle_manage_stack_site_flow() { show_warning_and_wait "Cannot manage site because stack metadata, env, or compose inputs are incomplete." 4 ;; 55) - show_warning_and_wait "Could not create the site. Check the output above for bench new-site details." 4 + show_warning_and_wait "Could not create the site. ${EASY_DOCKER_SITE_ERROR_DETAIL:-Check the output above for bench new-site details.} ${EASY_DOCKER_SITE_ERROR_LOG_PATH:+See ${stack_dir}/${EASY_DOCKER_SITE_ERROR_LOG_PATH}}" 6 ;; 56) - show_warning_and_wait "The site was created, but app installation failed. Check the output above." 4 + show_warning_and_wait "The site was created, but app installation failed. ${EASY_DOCKER_SITE_ERROR_DETAIL:-Check the output above.} ${EASY_DOCKER_SITE_ERROR_LOG_PATH:+See ${stack_dir}/${EASY_DOCKER_SITE_ERROR_LOG_PATH}}" 6 ;; 57) show_warning_and_wait "Site bootstrap currently supports only MariaDB-backed single-host stacks." 4 @@ -204,6 +202,9 @@ handle_manage_stack_site_flow() { 62) show_warning_and_wait "Cannot prepare the stack for site creation because the bench runtime files could not be repaired." 4 ;; + 63) + show_warning_and_wait "Cannot install the selected stack apps because at least one app is missing from the backend image. ${EASY_DOCKER_SITE_ERROR_DETAIL} ${EASY_DOCKER_SITE_ERROR_LOG_PATH:+See ${stack_dir}/${EASY_DOCKER_SITE_ERROR_LOG_PATH}}" 7 + ;; *) show_warning_and_wait "Site bootstrap failed (${site_flow_status})." 4 ;; @@ -220,6 +221,9 @@ handle_manage_stack_site_flow() { existing_site_name="$(get_stack_site_name "${stack_dir}" || true)" existing_site_created_at="$(get_stack_site_created_at "${stack_dir}" || true)" existing_site_apps_lines="$(get_stack_site_apps_installed_lines "${stack_dir}" || true)" + if stack_backend_service_is_running "${stack_dir}" >/dev/null 2>&1; then + get_stack_site_runtime_selected_apps_lines existing_site_apps_lines "${stack_dir}" "${existing_site_name}" || true + fi if [ -n "${existing_site_apps_lines}" ]; then existing_site_apps_csv="$(printf '%s' "${existing_site_apps_lines}" | tr '\n' ',' | sed 's/,$//')" else @@ -231,7 +235,6 @@ handle_manage_stack_site_flow() { "${stack_name}" \ "${stack_dir}" \ "${existing_site_name}" \ - "${site_status_label}" \ "${existing_site_created_at}" \ "${existing_site_apps_csv}" || true )" diff --git a/scripts/easy-docker/lib/ui/screens/production/manage.sh b/scripts/easy-docker/lib/ui/screens/production/manage.sh index 9bc59be4..5c2a3009 100755 --- a/scripts/easy-docker/lib/ui/screens/production/manage.sh +++ b/scripts/easy-docker/lib/ui/screens/production/manage.sh @@ -125,13 +125,12 @@ show_manage_stack_docker_menu() { show_manage_stack_site_menu() { local stack_name="${1}" local stack_dir="${2}" - local site_status="${3:-Not configured}" - local existing_site_entry="${4:-}" + local existing_site_entry="${3:-}" local status_text="" render_main_screen 1 >&2 - status_text="$(printf "Manage stack site\n\nStack: %s\nDirectory: %s\nSite status: %s\n\nCreate a new site or select an existing site for this stack." "${stack_name}" "${stack_dir}" "${site_status}")" + status_text="$(printf "Manage stack site\n\nStack: %s\nDirectory: %s\n\nCreate a new site or select an existing site for this stack." "${stack_name}" "${stack_dir}")" render_box_message "${status_text}" "0 2" >&2 if [ -n "${existing_site_entry}" ]; then @@ -192,18 +191,17 @@ show_manage_stack_site_details() { local stack_name="${1}" local stack_dir="${2}" local site_name="${3}" - local site_status="${4:-Unknown}" - local created_at="${5:-}" - local installed_apps="${6:-None}" + local created_at="${4:-}" + local installed_apps="${5:-None}" local status_text="" render_main_screen 1 >&2 - status_text="$(printf "Manage existing site\n\nStack: %s\nDirectory: %s\nSite: %s\nStatus: %s\nCreated at: %s\nInstalled apps: %s" "${stack_name}" "${stack_dir}" "${site_name}" "${site_status}" "${created_at:-n/a}" "${installed_apps}")" + status_text="$(printf "Manage existing site\n\nStack: %s\nDirectory: %s\nSite: %s\nCreated at: %s\nInstalled apps: %s" "${stack_name}" "${stack_dir}" "${site_name}" "${created_at:-n/a}" "${installed_apps}")" render_box_message "${status_text}" "0 2" >&2 gum choose \ - --height 7 \ + --height 8 \ --header "Site details" \ --cursor.foreground 63 \ --selected.foreground 45 \