diff --git a/scripts/easy-docker/lib/app/wizard/common/compose.sh b/scripts/easy-docker/lib/app/wizard/common/compose.sh index f890efe5..9bfc7f56 100755 --- a/scripts/easy-docker/lib/app/wizard/common/compose.sh +++ b/scripts/easy-docker/lib/app/wizard/common/compose.sh @@ -4,420 +4,16 @@ EASY_DOCKER_BUILD_ERROR_DETAIL="" # shellcheck disable=SC2034 # Read by manage flow after start_stack_with_compose_from_metadata fails. EASY_DOCKER_COMPOSE_ERROR_DETAIL="" -render_stack_compose_from_metadata() { - local stack_dir="${1}" - local metadata_path="" - local env_path="" - local generated_compose_path="" - local generated_compose_tmp_path="" - local compose_files_lines="" - local compose_file="" - local source_compose_path="" - local env_erpnext_version="" - local fallback_erpnext_version="" - local repo_root="" - local -a compose_args=() +load_easy_docker_compose_modules() { + local compose_dir="" + compose_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/compose" - metadata_path="${stack_dir}/metadata.json" - env_path="$(get_stack_env_path "${stack_dir}")" - generated_compose_path="$(get_stack_generated_compose_path "${stack_dir}")" - generated_compose_tmp_path="${generated_compose_path}.tmp" - - if [ ! -f "${metadata_path}" ]; then - return 1 - fi - - if [ ! -f "${env_path}" ]; then - return 1 - fi - - env_erpnext_version="$(get_env_file_key_value "${env_path}" "ERPNEXT_VERSION" || true)" - if [ -z "${env_erpnext_version}" ]; then - fallback_erpnext_version="$(get_default_erpnext_version || true)" - fi - - compose_files_lines="$(get_metadata_compose_files_lines "${metadata_path}" || true)" - if [ -z "${compose_files_lines}" ]; then - return 1 - fi - - repo_root="$(get_easy_docker_repo_root)" - while IFS= read -r compose_file; do - if [ -z "${compose_file}" ]; then - continue - fi - - source_compose_path="${repo_root}/${compose_file}" - if [ ! -f "${source_compose_path}" ]; then - return 1 - fi - - compose_args+=(-f "${source_compose_path}") - done <"${generated_compose_tmp_path}"; then - rm -f -- "${generated_compose_tmp_path}" >/dev/null 2>&1 || true - return 1 - fi - elif ! docker compose --env-file "${env_path}" "${compose_args[@]}" config >"${generated_compose_tmp_path}"; then - rm -f -- "${generated_compose_tmp_path}" >/dev/null 2>&1 || true - return 1 - fi - - if ! mv -- "${generated_compose_tmp_path}" "${generated_compose_path}"; then - rm -f -- "${generated_compose_tmp_path}" >/dev/null 2>&1 || true - return 1 - fi - - return 0 + # shellcheck source=scripts/easy-docker/lib/app/wizard/common/compose/render.sh + source "${compose_dir}/render.sh" + # shellcheck source=scripts/easy-docker/lib/app/wizard/common/compose/start.sh + source "${compose_dir}/start.sh" + # shellcheck source=scripts/easy-docker/lib/app/wizard/common/compose/build.sh + source "${compose_dir}/build.sh" } -start_stack_with_compose_from_metadata() { - local stack_dir="${1}" - local metadata_path="" - local env_path="" - local compose_files_lines="" - local compose_file="" - local source_compose_path="" - local env_erpnext_version="" - local fallback_erpnext_version="" - local configured_pull_policy="" - local runtime_pull_policy="" - local custom_image="" - local custom_tag="" - local image_ref="" - local stack_topology="" - local repo_root="" - local -a compose_args=() - - # shellcheck disable=SC2034 # Read by manage flow after start_stack_with_compose_from_metadata fails. - EASY_DOCKER_COMPOSE_ERROR_DETAIL="" - - metadata_path="${stack_dir}/metadata.json" - env_path="$(get_stack_env_path "${stack_dir}")" - - if [ ! -f "${metadata_path}" ]; then - return 31 - fi - - if [ ! -f "${env_path}" ]; then - return 32 - fi - - stack_topology="$(get_stack_topology "${stack_dir}" || true)" - if [ -z "${stack_topology}" ]; then - # shellcheck disable=SC2034 # Read by manage flow after start_stack_with_compose_from_metadata returns 33. - EASY_DOCKER_COMPOSE_ERROR_DETAIL="metadata.json missing topology" - return 33 - fi - - case "${stack_topology}" in - "single-host") ;; - *) - # shellcheck disable=SC2034 # Read by manage flow after start_stack_with_compose_from_metadata returns 34. - EASY_DOCKER_COMPOSE_ERROR_DETAIL="${stack_topology}" - return 34 - ;; - esac - - env_erpnext_version="$(get_env_file_key_value "${env_path}" "ERPNEXT_VERSION" || true)" - if [ -z "${env_erpnext_version}" ]; then - fallback_erpnext_version="$(get_default_erpnext_version || true)" - fi - - configured_pull_policy="$(get_env_file_key_value "${env_path}" "PULL_POLICY" || true)" - if [ -z "${configured_pull_policy}" ]; then - custom_image="$(get_env_file_key_value "${env_path}" "CUSTOM_IMAGE" || true)" - custom_tag="$(get_env_file_key_value "${env_path}" "CUSTOM_TAG" || true)" - if [ -n "${custom_image}" ] && [ -n "${custom_tag}" ]; then - image_ref="${custom_image}:${custom_tag}" - if docker image inspect "${image_ref}" >/dev/null 2>&1; then - runtime_pull_policy="if_not_present" - fi - fi - fi - - compose_files_lines="$(get_metadata_compose_files_lines "${metadata_path}" || true)" - if [ -z "${compose_files_lines}" ]; then - return 35 - fi - - repo_root="$(get_easy_docker_repo_root)" - while IFS= read -r compose_file; do - if [ -z "${compose_file}" ]; then - continue - fi - - source_compose_path="${repo_root}/${compose_file}" - if [ ! -f "${source_compose_path}" ]; then - # shellcheck disable=SC2034 # Read by manage flow after start_stack_with_compose_from_metadata returns 36. - EASY_DOCKER_COMPOSE_ERROR_DETAIL="${source_compose_path}" - return 36 - fi - - compose_args+=(-f "${source_compose_path}") - done </dev/null - )" - compose_status=$? - else - running_services_lines="$( - docker compose --env-file "${env_path}" "${compose_args[@]}" ps --status running --services 2>/dev/null - )" - compose_status=$? - fi - - if [ "${compose_status}" -ne 0 ]; then - printf -v "${result_var}" "%s" "Unknown (docker compose status failed)" - return 0 - fi - - while IFS= read -r compose_file; do - if [ -n "${compose_file}" ]; then - running_services_count=$((running_services_count + 1)) - fi - done <&1)"; then - : - else - # shellcheck disable=SC2034 # Read by manage flow after build_stack_custom_image returns 24. - EASY_DOCKER_BUILD_ERROR_DETAIL="$(printf '%s@%s :: %s' "${app_url}" "${app_branch}" "${git_error}")" - return 24 - fi - done <&1)"; then + : + else + # shellcheck disable=SC2034 # Read by manage flow after build_stack_custom_image returns 24. + EASY_DOCKER_BUILD_ERROR_DETAIL="$(printf '%s@%s :: %s' "${app_url}" "${app_branch}" "${git_error}")" + return 24 + fi + done <"${generated_compose_tmp_path}"; then + rm -f -- "${generated_compose_tmp_path}" >/dev/null 2>&1 || true + return 1 + fi + elif ! docker compose --env-file "${env_path}" "${compose_args[@]}" config >"${generated_compose_tmp_path}"; then + rm -f -- "${generated_compose_tmp_path}" >/dev/null 2>&1 || true + return 1 + fi + + if ! mv -- "${generated_compose_tmp_path}" "${generated_compose_path}"; then + rm -f -- "${generated_compose_tmp_path}" >/dev/null 2>&1 || true + return 1 + fi + + return 0 +} diff --git a/scripts/easy-docker/lib/app/wizard/common/compose/start.sh b/scripts/easy-docker/lib/app/wizard/common/compose/start.sh new file mode 100755 index 00000000..2cfde7e7 --- /dev/null +++ b/scripts/easy-docker/lib/app/wizard/common/compose/start.sh @@ -0,0 +1,242 @@ +#!/usr/bin/env bash + +start_stack_with_compose_from_metadata() { + local stack_dir="${1}" + local metadata_path="" + local env_path="" + local compose_files_lines="" + local compose_file="" + local source_compose_path="" + local env_erpnext_version="" + local fallback_erpnext_version="" + local configured_pull_policy="" + local runtime_pull_policy="" + local custom_image="" + local custom_tag="" + local image_ref="" + local image_inspect_error="" + local stack_topology="" + local repo_root="" + local -a compose_args=() + + # shellcheck disable=SC2034 # Read by manage flow after start_stack_with_compose_from_metadata fails. + EASY_DOCKER_COMPOSE_ERROR_DETAIL="" + + metadata_path="${stack_dir}/metadata.json" + env_path="$(get_stack_env_path "${stack_dir}")" + + if [ ! -f "${metadata_path}" ]; then + return 31 + fi + + if [ ! -f "${env_path}" ]; then + return 32 + fi + + stack_topology="$(get_stack_topology "${stack_dir}" || true)" + if [ -z "${stack_topology}" ]; then + # shellcheck disable=SC2034 # Read by manage flow after start_stack_with_compose_from_metadata returns 33. + EASY_DOCKER_COMPOSE_ERROR_DETAIL="metadata.json missing topology" + return 33 + fi + + case "${stack_topology}" in + "single-host") ;; + *) + # shellcheck disable=SC2034 # Read by manage flow after start_stack_with_compose_from_metadata returns 34. + EASY_DOCKER_COMPOSE_ERROR_DETAIL="${stack_topology}" + return 34 + ;; + esac + + env_erpnext_version="$(get_env_file_key_value "${env_path}" "ERPNEXT_VERSION" || true)" + if [ -z "${env_erpnext_version}" ]; then + fallback_erpnext_version="$(get_default_erpnext_version || true)" + fi + + configured_pull_policy="$(get_env_file_key_value "${env_path}" "PULL_POLICY" || true)" + if [ -z "${configured_pull_policy}" ]; then + custom_image="$(get_env_file_key_value "${env_path}" "CUSTOM_IMAGE" || true)" + custom_tag="$(get_env_file_key_value "${env_path}" "CUSTOM_TAG" || true)" + if [ -n "${custom_image}" ] && [ -n "${custom_tag}" ]; then + image_ref="${custom_image}:${custom_tag}" + if image_inspect_error="$(docker image inspect "${image_ref}" 2>&1 >/dev/null)"; then + runtime_pull_policy="if_not_present" + else + case "${image_inspect_error}" in + *"No such image"* | *"No such object"*) + # shellcheck disable=SC2034 # Read by manage flow after start_stack_with_compose_from_metadata returns 38. + EASY_DOCKER_COMPOSE_ERROR_DETAIL="${image_ref}" + return 38 + ;; + *) + if [ -z "${image_inspect_error}" ]; then + image_inspect_error="docker image inspect failed for ${image_ref}" + fi + # shellcheck disable=SC2034 # Read by manage flow after start_stack_with_compose_from_metadata returns 39. + EASY_DOCKER_COMPOSE_ERROR_DETAIL="${image_inspect_error}" + return 39 + ;; + esac + fi + fi + fi + + compose_files_lines="$(get_metadata_compose_files_lines "${metadata_path}" || true)" + if [ -z "${compose_files_lines}" ]; then + return 35 + fi + + repo_root="$(get_easy_docker_repo_root)" + while IFS= read -r compose_file; do + if [ -z "${compose_file}" ]; then + continue + fi + + source_compose_path="${repo_root}/${compose_file}" + if [ ! -f "${source_compose_path}" ]; then + # shellcheck disable=SC2034 # Read by manage flow after start_stack_with_compose_from_metadata returns 36. + EASY_DOCKER_COMPOSE_ERROR_DETAIL="${source_compose_path}" + return 36 + fi + + compose_args+=(-f "${source_compose_path}") + done </dev/null + )" + compose_status=$? + else + running_services_lines="$( + docker compose --env-file "${env_path}" "${compose_args[@]}" ps --status running --services 2>/dev/null + )" + compose_status=$? + fi + + if [ "${compose_status}" -ne 0 ]; then + printf -v "${result_var}" "%s" "Unknown (docker compose status failed)" + return 0 + fi + + while IFS= read -r compose_file; do + if [ -n "${compose_file}" ]; then + running_services_count=$((running_services_count + 1)) + fi + done <&2 - - setup_label="$(get_setup_display_label "${setup_type}")" - status_text="$(printf "%s stack\n\nChoose whether to create a new stack or manage an existing one." "${setup_label}")" - - render_box_message "${status_text}" "0 2" >&2 - - gum choose \ - --height 8 \ - --header "${setup_label} stack actions" \ - --cursor.foreground 63 \ - --selected.foreground 45 \ - "Create new stack" \ - "Manage existing stacks" \ - "Back" \ - "Exit and close easy-docker" -} - -show_production_setup_menu() { - show_setup_menu "production" -} - -show_development_setup_menu() { - show_setup_menu "development" -} - -prompt_new_stack_name() { - local status_text="" - - render_main_screen 1 >&2 - - status_text="$(printf "Create new stack\n\nEnter a stack name.\nType /cancel or press Ctrl+C to abort.")" - - render_box_message "${status_text}" "0 2" >&2 - - gum input \ - --header "Stack name (/cancel to abort)" \ - --prompt "name> " \ - --placeholder "my-production-stack" -} - -show_frappe_version_profile_menu() { - local stack_name="${1}" - local options_lines="${2:-}" - local selected_label="${3:-}" - local status_text="" - local option_line="" - local -a menu_options=() - local -a gum_args=() - - render_main_screen 1 >&2 - - status_text="$(printf "Create stack: %s\n\nSelect the Frappe branch profile from frappe.tsv.\nThis sets the stack default for branch suggestions." "${stack_name}")" - render_box_message "${status_text}" "0 2" >&2 - - while IFS= read -r option_line; do - if [ -z "${option_line}" ]; then - continue - fi - menu_options+=("${option_line}") - done <&2 - - stack_name="${stack_dir##*/}" - status_text="$(printf "Stack created: %s\nDirectory: %s\n\nChoose the deployment topology.\n\n- Single-host: easiest setup on one server.\n- Split services: separate app and infra stacks for more control." "${stack_name}" "${stack_dir}")" - render_box_message "${status_text}" "0 2" >&2 - - gum choose \ - --height 8 \ - --header "Topology" \ - --cursor.foreground 63 \ - --selected.foreground 45 \ - "Single-host (recommended)" \ - "Split services" \ - "Abort wizard to main menu" -} - -show_single_host_proxy_menu() { - local stack_dir="${1}" - local stack_name="" - local status_text="" - - render_main_screen 1 >&2 - - stack_name="${stack_dir##*/}" - status_text="$(printf "Stack: %s\n\nSingle-host setup (step 1/3)\nChoose the proxy mode.\n\n- Traefik and nginx-proxy run inside compose.\n- Caddy is external and uses the no-proxy compose mode." "${stack_name}")" - render_box_message "${status_text}" "0 2" >&2 - - gum choose \ - --height 11 \ - --header "Single-host: proxy mode" \ - --cursor.foreground 63 \ - --selected.foreground 45 \ - "Traefik (HTTP, built-in proxy)" \ - "Traefik (HTTPS + Let's Encrypt)" \ - "nginx-proxy (HTTP)" \ - "nginx-proxy + acme-companion (HTTPS)" \ - "Caddy (external reverse proxy)" \ - "No reverse proxy (direct :8080)" \ - "Back to topology selection" -} - -show_single_host_database_menu() { - local stack_dir="${1}" - local stack_name="" - local status_text="" - - render_main_screen 1 >&2 - - stack_name="${stack_dir##*/}" - status_text="$(printf "Stack: %s\n\nSingle-host setup (step 2/3)\nChoose the database service." "${stack_name}")" - render_box_message "${status_text}" "0 2" >&2 - - gum choose \ - --height 8 \ - --header "Single-host: database" \ - --cursor.foreground 63 \ - --selected.foreground 45 \ - "MariaDB (recommended)" \ - "PostgreSQL" \ - "Back to topology selection" -} - -show_single_host_redis_menu() { - local stack_dir="${1}" - local stack_name="" - local status_text="" - - render_main_screen 1 >&2 - - stack_name="${stack_dir##*/}" - status_text="$(printf "Stack: %s\n\nSingle-host setup (step 3/3)\nChoose whether Redis services should be included." "${stack_name}")" - render_box_message "${status_text}" "0 2" >&2 - - gum choose \ - --height 8 \ - --header "Single-host: redis" \ - --cursor.foreground 63 \ - --selected.foreground 45 \ - "Include Redis (recommended)" \ - "Skip Redis (experienced users only)" \ - "Back to topology selection" -} - -show_custom_modular_apps_multi_select() { - local stack_dir="${1}" - local options_lines="${2:-}" - local selected_labels_csv="${3:-}" - local stack_name="" - local status_text="" - local option_line="" - local selected_label="" - local -a menu_options=() - local -a selected_labels=() - local -a gum_args=() - - render_main_screen 1 >&2 - - stack_name="${stack_dir##*/}" - status_text="$(printf "Stack: %s\n\nApps\nUse Space to toggle apps from apps.tsv. Press Enter to continue to branch selection per app.\nUse Ctrl+C to go back." "${stack_name}")" - render_box_message "${status_text}" "0 2" >&2 - - while IFS= read -r option_line; do - if [ -z "${option_line}" ]; then - continue - fi - menu_options+=("${option_line}") - done <&2 - - stack_name="${stack_dir##*/}" - guidance_text="${guidance_text//\\n/$'\n'}" - status_text="$(printf "Stack: %s\n\nConfigure %s\n\n%s" "${stack_name}" "${variable_name}" "${guidance_text}")" - render_box_message "${status_text}" "0 2" >&2 - fi - - if [ -n "${input_feedback}" ]; then - gum style --foreground 214 "${input_feedback}" >&2 - fi - - gum input \ - --header "${variable_name}" \ - --prompt "value> " \ - --placeholder "${placeholder}" -} - -show_split_services_examples() { - local status_text="" - - render_main_screen 1 >&2 - - status_text="$(printf "Split services examples\n\n- DB in a separate stack/project.\n- Proxy in a separate stack/project.\n- One or more app stacks referencing shared infra.")" - render_box_message "${status_text}" "0 2" >&2 - - gum choose \ - --height 7 \ - --header "Split services" \ - --cursor.foreground 63 \ - --selected.foreground 45 \ - "Use this topology" \ - "Back to topology selection" -} - -show_abort_wizard_prompt() { - local stack_dir="${1}" - local status_text="" - - render_main_screen 1 >&2 - - status_text="$(printf "Abort wizard\n\nStack directory:\n%s\n\nRollback created files before returning to main menu?" "${stack_dir}")" - render_box_message "${status_text}" "0 2" >&2 - - gum choose \ - --height 8 \ - --header "Abort options" \ - --cursor.foreground 63 \ - --selected.foreground 45 \ - "Rollback files and return to main menu" \ - "Keep files and return to main menu" \ - "Back to topology selection" -} - -show_manage_stacks_menu() { - local setup_type="${1}" - shift - local stack_count="${#}" - local setup_label="" - local status_text="" - - render_main_screen 1 >&2 - - setup_label="$(get_setup_display_label "${setup_type}")" - if [ "${stack_count}" -eq 1 ]; then - status_text="$(printf "Manage existing %s stacks\n\n1 stack found. Select a stack." "${setup_label}")" - else - status_text="$(printf "Manage existing %s stacks\n\n%s stacks found. Select a stack." "${setup_label}" "${stack_count}")" - fi - - render_box_message "${status_text}" "0 2" >&2 - - gum choose \ - --height 14 \ - --header "Existing stacks" \ - --cursor.foreground 63 \ - --selected.foreground 45 \ - "$@" \ - "Back" \ - "Exit and close easy-docker" -} - -show_manage_stacks_placeholder() { - local setup_type="${1}" - local setup_label="" - local status_text="" - - render_main_screen 1 >&2 - - setup_label="$(get_setup_display_label "${setup_type}")" - status_text="$(printf "Manage existing %s stacks\n\nNo stacks found in .easy-docker/stacks yet." "${setup_label}")" - - render_box_message "${status_text}" "0 2" >&2 - - gum choose \ - --height 7 \ - --header "Manage stacks actions" \ - --cursor.foreground 63 \ - --selected.foreground 45 \ - "Back" \ - "Exit and close easy-docker" -} - -show_manage_stack_actions_menu() { - local stack_name="${1}" - local stack_dir="${2}" - local stack_runtime_status="${3:-Unknown}" - local menu_header="" - local status_text="" - - render_main_screen 1 >&2 - - status_text="$(printf "Manage stack\n\nStack: %s\nDirectory: %s\nRuntime status: %s\n\nChoose an action for this stack." "${stack_name}" "${stack_dir}" "${stack_runtime_status}")" - - render_box_message "${status_text}" "0 2" >&2 - - menu_header="$(printf "Stack actions | %s" "${stack_runtime_status}")" - - gum choose \ - --height 8 \ - --header "${menu_header}" \ - --cursor.foreground 63 \ - --selected.foreground 45 \ - "Apps" \ - "Docker" \ - "Start stack in Docker Compose" \ - "Back" \ - "Exit and close easy-docker" -} - -show_manage_stack_apps_menu() { - local stack_name="${1}" - local stack_dir="${2}" - local status_text="" - - render_main_screen 1 >&2 - - status_text="$(printf "Manage stack apps\n\nStack: %s\nDirectory: %s\n\nChoose an app-related action for this stack." "${stack_name}" "${stack_dir}")" - - render_box_message "${status_text}" "0 2" >&2 - - gum choose \ - --height 8 \ - --header "Stack apps actions" \ - --cursor.foreground 63 \ - --selected.foreground 45 \ - "Regenerate apps.json from metadata" \ - "Select apps and branches" \ - "Back" \ - "Exit and close easy-docker" -} - -show_manage_stack_docker_menu() { - local stack_name="${1}" - local stack_dir="${2}" - local status_text="" - - render_main_screen 1 >&2 - - status_text="$(printf "Manage stack docker\n\nStack: %s\nDirectory: %s\n\nChoose a docker-related action for this stack." "${stack_name}" "${stack_dir}")" - - render_box_message "${status_text}" "0 2" >&2 - - gum choose \ - --height 8 \ - --header "Stack docker actions" \ - --cursor.foreground 63 \ - --selected.foreground 45 \ - "Build custom image" \ - "Generate docker compose from env" \ - "Back" \ - "Exit and close easy-docker" -} +load_production_screen_modules diff --git a/scripts/easy-docker/lib/ui/screens/production/manage.sh b/scripts/easy-docker/lib/ui/screens/production/manage.sh new file mode 100755 index 00000000..d61ecc58 --- /dev/null +++ b/scripts/easy-docker/lib/ui/screens/production/manage.sh @@ -0,0 +1,142 @@ +#!/usr/bin/env bash + +show_manage_stacks_menu() { + local setup_type="${1}" + shift + local stack_count="${#}" + local setup_label="" + local status_text="" + + render_main_screen 1 >&2 + + setup_label="$(get_setup_display_label "${setup_type}")" + if [ "${stack_count}" -eq 1 ]; then + status_text="$(printf "Manage existing %s stacks\n\n1 stack found. Select a stack." "${setup_label}")" + else + status_text="$(printf "Manage existing %s stacks\n\n%s stacks found. Select a stack." "${setup_label}" "${stack_count}")" + fi + + render_box_message "${status_text}" "0 2" >&2 + + gum choose \ + --height 14 \ + --header "Existing stacks" \ + --cursor.foreground 63 \ + --selected.foreground 45 \ + "$@" \ + "Back" \ + "Exit and close easy-docker" +} + +show_manage_stacks_placeholder() { + local setup_type="${1}" + local setup_label="" + local status_text="" + + render_main_screen 1 >&2 + + setup_label="$(get_setup_display_label "${setup_type}")" + status_text="$(printf "Manage existing %s stacks\n\nNo stacks found in .easy-docker/stacks yet." "${setup_label}")" + + render_box_message "${status_text}" "0 2" >&2 + + gum choose \ + --height 7 \ + --header "Manage stacks actions" \ + --cursor.foreground 63 \ + --selected.foreground 45 \ + "Back" \ + "Exit and close easy-docker" +} + +show_manage_stack_actions_menu() { + local stack_name="${1}" + local stack_dir="${2}" + local stack_runtime_status="${3:-Unknown}" + local menu_header="" + local status_text="" + + render_main_screen 1 >&2 + + status_text="$(printf "Manage stack\n\nStack: %s\nDirectory: %s\nRuntime status: %s\n\nChoose an action for this stack." "${stack_name}" "${stack_dir}" "${stack_runtime_status}")" + + render_box_message "${status_text}" "0 2" >&2 + + menu_header="$(printf "Stack actions | %s" "${stack_runtime_status}")" + + gum choose \ + --height 8 \ + --header "${menu_header}" \ + --cursor.foreground 63 \ + --selected.foreground 45 \ + "Apps" \ + "Docker" \ + "Start stack in Docker Compose" \ + "Back" \ + "Exit and close easy-docker" +} + +show_manage_stack_apps_menu() { + local stack_name="${1}" + local stack_dir="${2}" + local status_text="" + + render_main_screen 1 >&2 + + status_text="$(printf "Manage stack apps\n\nStack: %s\nDirectory: %s\n\nChoose an app-related action for this stack." "${stack_name}" "${stack_dir}")" + + render_box_message "${status_text}" "0 2" >&2 + + gum choose \ + --height 8 \ + --header "Stack apps actions" \ + --cursor.foreground 63 \ + --selected.foreground 45 \ + "Regenerate apps.json from metadata" \ + "Select apps and branches" \ + "Back" \ + "Exit and close easy-docker" +} + +show_manage_stack_docker_menu() { + local stack_name="${1}" + local stack_dir="${2}" + local status_text="" + + render_main_screen 1 >&2 + + status_text="$(printf "Manage stack docker\n\nStack: %s\nDirectory: %s\n\nChoose a docker-related action for this stack." "${stack_name}" "${stack_dir}")" + + render_box_message "${status_text}" "0 2" >&2 + + gum choose \ + --height 8 \ + --header "Stack docker actions" \ + --cursor.foreground 63 \ + --selected.foreground 45 \ + "Build custom image" \ + "Generate docker compose from env" \ + "Back" \ + "Exit and close easy-docker" +} + +show_missing_custom_image_start_menu() { + local stack_name="${1}" + local stack_dir="${2}" + local image_ref="${3}" + local status_text="" + + render_main_screen 1 >&2 + + status_text="$(printf "Custom image missing\n\nStack: %s\nDirectory: %s\nImage: %s\n\nBuild the custom image now before starting the stack?" "${stack_name}" "${stack_dir}" "${image_ref}")" + render_box_message "${status_text}" "0 2" >&2 + + gum choose \ + --height 8 \ + --header "Missing custom image" \ + --cursor.foreground 63 \ + --selected.foreground 45 \ + "Build custom image now" \ + "Back" \ + "Exit and close easy-docker" +} diff --git a/scripts/easy-docker/lib/ui/screens/production/setup.sh b/scripts/easy-docker/lib/ui/screens/production/setup.sh new file mode 100755 index 00000000..a03a51c5 --- /dev/null +++ b/scripts/easy-docker/lib/ui/screens/production/setup.sh @@ -0,0 +1,100 @@ +#!/usr/bin/env bash + +get_setup_display_label() { + local setup_type="${1}" + + case "${setup_type}" in + development) + printf 'Development' + ;; + production | *) + printf 'Production' + ;; + esac +} + +show_setup_menu() { + local setup_type="${1}" + local setup_label="" + local status_text="" + + render_main_screen 1 >&2 + + setup_label="$(get_setup_display_label "${setup_type}")" + status_text="$(printf "%s stack\n\nChoose whether to create a new stack or manage an existing one." "${setup_label}")" + + render_box_message "${status_text}" "0 2" >&2 + + gum choose \ + --height 8 \ + --header "${setup_label} stack actions" \ + --cursor.foreground 63 \ + --selected.foreground 45 \ + "Create new stack" \ + "Manage existing stacks" \ + "Back" \ + "Exit and close easy-docker" +} + +show_production_setup_menu() { + show_setup_menu "production" +} + +show_development_setup_menu() { + show_setup_menu "development" +} + +prompt_new_stack_name() { + local status_text="" + + render_main_screen 1 >&2 + + status_text="$(printf "Create new stack\n\nEnter a stack name.\nType /cancel or press Ctrl+C to abort.")" + + render_box_message "${status_text}" "0 2" >&2 + + gum input \ + --header "Stack name (/cancel to abort)" \ + --prompt "name> " \ + --placeholder "my-production-stack" +} + +show_frappe_version_profile_menu() { + local stack_name="${1}" + local options_lines="${2:-}" + local selected_label="${3:-}" + local status_text="" + local option_line="" + local -a menu_options=() + local -a gum_args=() + + render_main_screen 1 >&2 + + status_text="$(printf "Create stack: %s\n\nSelect the Frappe branch profile from frappe.tsv.\nThis sets the stack default for branch suggestions." "${stack_name}")" + render_box_message "${status_text}" "0 2" >&2 + + while IFS= read -r option_line; do + if [ -z "${option_line}" ]; then + continue + fi + menu_options+=("${option_line}") + done <&2 + + stack_name="${stack_dir##*/}" + status_text="$(printf "Stack created: %s\nDirectory: %s\n\nChoose the deployment topology.\n\n- Single-host: easiest setup on one server.\n- Split services: separate app and infra stacks for more control." "${stack_name}" "${stack_dir}")" + render_box_message "${status_text}" "0 2" >&2 + + gum choose \ + --height 8 \ + --header "Topology" \ + --cursor.foreground 63 \ + --selected.foreground 45 \ + "Single-host (recommended)" \ + "Split services" \ + "Abort wizard to main menu" +} + +show_single_host_proxy_menu() { + local stack_dir="${1}" + local stack_name="" + local status_text="" + + render_main_screen 1 >&2 + + stack_name="${stack_dir##*/}" + status_text="$(printf "Stack: %s\n\nSingle-host setup (step 1/3)\nChoose the proxy mode.\n\n- Traefik and nginx-proxy run inside compose.\n- Caddy is external and uses the no-proxy compose mode." "${stack_name}")" + render_box_message "${status_text}" "0 2" >&2 + + gum choose \ + --height 11 \ + --header "Single-host: proxy mode" \ + --cursor.foreground 63 \ + --selected.foreground 45 \ + "Traefik (HTTP, built-in proxy)" \ + "Traefik (HTTPS + Let's Encrypt)" \ + "nginx-proxy (HTTP)" \ + "nginx-proxy + acme-companion (HTTPS)" \ + "Caddy (external reverse proxy)" \ + "No reverse proxy (direct :8080)" \ + "Back to topology selection" +} + +show_single_host_database_menu() { + local stack_dir="${1}" + local stack_name="" + local status_text="" + + render_main_screen 1 >&2 + + stack_name="${stack_dir##*/}" + status_text="$(printf "Stack: %s\n\nSingle-host setup (step 2/3)\nChoose the database service." "${stack_name}")" + render_box_message "${status_text}" "0 2" >&2 + + gum choose \ + --height 8 \ + --header "Single-host: database" \ + --cursor.foreground 63 \ + --selected.foreground 45 \ + "MariaDB (recommended)" \ + "PostgreSQL" \ + "Back to topology selection" +} + +show_single_host_redis_menu() { + local stack_dir="${1}" + local stack_name="" + local status_text="" + + render_main_screen 1 >&2 + + stack_name="${stack_dir##*/}" + status_text="$(printf "Stack: %s\n\nSingle-host setup (step 3/3)\nChoose whether Redis services should be included." "${stack_name}")" + render_box_message "${status_text}" "0 2" >&2 + + gum choose \ + --height 8 \ + --header "Single-host: redis" \ + --cursor.foreground 63 \ + --selected.foreground 45 \ + "Include Redis (recommended)" \ + "Skip Redis (experienced users only)" \ + "Back to topology selection" +} + +show_custom_modular_apps_multi_select() { + local stack_dir="${1}" + local options_lines="${2:-}" + local selected_labels_csv="${3:-}" + local stack_name="" + local status_text="" + local option_line="" + local selected_label="" + local -a menu_options=() + local -a selected_labels=() + local -a gum_args=() + + render_main_screen 1 >&2 + + stack_name="${stack_dir##*/}" + status_text="$(printf "Stack: %s\n\nApps\nUse Space to toggle apps from apps.tsv. Press Enter to continue to branch selection per app.\nUse Ctrl+C to go back." "${stack_name}")" + render_box_message "${status_text}" "0 2" >&2 + + while IFS= read -r option_line; do + if [ -z "${option_line}" ]; then + continue + fi + menu_options+=("${option_line}") + done <&2 + + stack_name="${stack_dir##*/}" + guidance_text="${guidance_text//\\n/$'\n'}" + status_text="$(printf "Stack: %s\n\nConfigure %s\n\n%s" "${stack_name}" "${variable_name}" "${guidance_text}")" + render_box_message "${status_text}" "0 2" >&2 + fi + + if [ -n "${input_feedback}" ]; then + gum style --foreground 214 "${input_feedback}" >&2 + fi + + gum input \ + --header "${variable_name}" \ + --prompt "value> " \ + --placeholder "${placeholder}" +} + +show_split_services_examples() { + local status_text="" + + render_main_screen 1 >&2 + + status_text="$(printf "Split services examples\n\n- DB in a separate stack/project.\n- Proxy in a separate stack/project.\n- One or more app stacks referencing shared infra.")" + render_box_message "${status_text}" "0 2" >&2 + + gum choose \ + --height 7 \ + --header "Split services" \ + --cursor.foreground 63 \ + --selected.foreground 45 \ + "Use this topology" \ + "Back to topology selection" +} + +show_abort_wizard_prompt() { + local stack_dir="${1}" + local status_text="" + + render_main_screen 1 >&2 + + status_text="$(printf "Abort wizard\n\nStack directory:\n%s\n\nRollback created files before returning to main menu?" "${stack_dir}")" + render_box_message "${status_text}" "0 2" >&2 + + gum choose \ + --height 8 \ + --header "Abort options" \ + --cursor.foreground 63 \ + --selected.foreground 45 \ + "Rollback files and return to main menu" \ + "Keep files and return to main menu" \ + "Back to topology selection" +}