refactor(easy-docker): modularize loader and production flow

This commit is contained in:
RocketQuack 2026-02-27 02:03:44 +01:00
parent 37122d20c1
commit 32f94c5f51
4 changed files with 447 additions and 144 deletions

View file

@ -1,5 +1,10 @@
#!/usr/bin/env bash
readonly FLOW_CONTINUE=0
readonly FLOW_BACK_TO_MAIN=10
readonly FLOW_EXIT_APP=11
readonly FLOW_ABORT_INPUT=12
get_easy_docker_repo_root() {
local app_lib_dir=""
app_lib_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
@ -10,6 +15,10 @@ get_easy_docker_stacks_dir() {
printf '%s/.easy-docker/stacks\n' "$(get_easy_docker_repo_root)"
}
get_current_utc_timestamp() {
date -u +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || date +"%Y-%m-%dT%H:%M:%SZ"
}
is_valid_stack_name() {
local stack_name="${1}"
@ -27,29 +36,69 @@ is_valid_stack_name() {
esac
}
create_stack_env_file() {
local result_var="${1}"
create_stack_directory_with_metadata() {
local stack_dir_var="${1}"
local stack_name="${2}"
local stacks_dir=""
local env_path=""
local created_stack_dir=""
local metadata_path=""
local created_at=""
stacks_dir="$(get_easy_docker_stacks_dir)"
env_path="${stacks_dir}/${stack_name}.env"
created_stack_dir="${stacks_dir}/${stack_name}"
metadata_path="${created_stack_dir}/metadata.json"
if ! mkdir -p "${stacks_dir}"; then
return 1
fi
if [ -e "${env_path}" ]; then
if [ -e "${created_stack_dir}" ]; then
return 2
fi
: >"${env_path}"
if ! mkdir -p "${created_stack_dir}"; then
return 1
fi
printf -v "${result_var}" "%s" "${env_path}"
created_at="$(get_current_utc_timestamp)"
if ! cat >"${metadata_path}" <<EOF; then
{
"schema_version": 1,
"stack_name": "${stack_name}",
"created_at": "${created_at}"
}
EOF
rollback_stack_directory "${created_stack_dir}" >/dev/null 2>&1 || true
return 1
fi
printf -v "${stack_dir_var}" "%s" "${created_stack_dir}"
return 0
}
rollback_stack_directory() {
local stack_dir="${1}"
local stacks_dir=""
if [ -z "${stack_dir}" ]; then
return 1
fi
stacks_dir="$(get_easy_docker_stacks_dir)"
case "${stack_dir}" in
"${stacks_dir}"/*) ;;
*)
return 2
;;
esac
if [ ! -d "${stack_dir}" ]; then
return 0
fi
rm -rf -- "${stack_dir}"
}
prompt_stack_name_with_cancel() {
local result_var="${1}"
local input_value=""
@ -58,38 +107,283 @@ prompt_stack_name_with_cancel() {
input_value="$(prompt_new_stack_name)"
input_status=$?
if [ "${input_status}" -ne 0 ]; then
return 3
return "${FLOW_ABORT_INPUT}"
fi
input_value="$(printf '%s' "${input_value}" | tr -d '\r\n')"
case "${input_value}" in
/cancel | /CANCEL | /Cancel)
return 3
return "${FLOW_ABORT_INPUT}"
;;
esac
printf -v "${result_var}" "%s" "${input_value}"
return 0
return "${FLOW_CONTINUE}"
}
show_warning_and_wait() {
local message="${1}"
local seconds="${2:-1}"
show_warning_message "${message}"
sleep "${seconds}"
}
handle_topology_examples_flow() {
local topology_name="${1}"
local detail_action=""
case "${topology_name}" in
"Single-host")
detail_action="$(show_single_host_examples || true)"
;;
"Split services")
detail_action="$(show_split_services_examples || true)"
;;
"Advanced")
detail_action="$(show_advanced_examples || true)"
;;
*)
show_warning_and_wait "Unknown topology: ${topology_name}"
return "${FLOW_CONTINUE}"
;;
esac
case "${detail_action}" in
"Use this topology")
show_warning_and_wait "Topology '${topology_name}' selected. Next wizard step is coming soon." 2
return "${FLOW_CONTINUE}"
;;
"Back to topology selection" | "")
return "${FLOW_CONTINUE}"
;;
*)
show_warning_and_wait "Unknown topology action: ${detail_action}"
return "${FLOW_CONTINUE}"
;;
esac
}
handle_abort_wizard_flow() {
local stack_dir="${1}"
local abort_action=""
local rollback_status=0
abort_action="$(show_abort_wizard_prompt "${stack_dir}" || true)"
case "${abort_action}" in
"Rollback files and return to main menu")
if rollback_stack_directory "${stack_dir}"; then
return "${FLOW_BACK_TO_MAIN}"
fi
rollback_status=$?
if [ "${rollback_status}" -eq 2 ]; then
show_warning_and_wait "Refused rollback for unsafe path: ${stack_dir}" 2
else
show_warning_and_wait "Could not rollback stack files: ${stack_dir}" 2
fi
return "${FLOW_CONTINUE}"
;;
"Keep files and return to main menu")
return "${FLOW_BACK_TO_MAIN}"
;;
"Back to topology selection" | "")
return "${FLOW_CONTINUE}"
;;
*)
show_warning_and_wait "Unknown abort action: ${abort_action}"
return "${FLOW_CONTINUE}"
;;
esac
}
handle_stack_topology_flow() {
local stack_dir="${1}"
local topology_action=""
local abort_status=0
while true; do
topology_action="$(show_stack_topology_menu "${stack_dir}" || true)"
case "${topology_action}" in
"Single-host" | "Split services" | "Advanced")
handle_topology_examples_flow "${topology_action}"
;;
"Abort wizard to main menu")
handle_abort_wizard_flow "${stack_dir}"
abort_status=$?
case "${abort_status}" in
"${FLOW_BACK_TO_MAIN}")
return "${FLOW_BACK_TO_MAIN}"
;;
*) ;;
esac
;;
"")
return "${FLOW_CONTINUE}"
;;
*)
show_warning_and_wait "Unknown topology selection: ${topology_action}"
;;
esac
done
}
handle_create_new_stack_flow() {
local stack_name=""
local stack_dir=""
local create_stack_status=0
local stack_input_status=0
local topology_status=0
while true; do
stack_name=""
if prompt_stack_name_with_cancel stack_name; then
:
else
stack_input_status=$?
if [ "${stack_input_status}" -eq "${FLOW_ABORT_INPUT}" ]; then
return "${FLOW_CONTINUE}"
fi
show_warning_and_wait "Input canceled."
return "${FLOW_CONTINUE}"
fi
if [ -z "${stack_name}" ]; then
return "${FLOW_CONTINUE}"
fi
if ! is_valid_stack_name "${stack_name}"; then
show_warning_and_wait "Invalid stack name. Use letters, numbers, dot, underscore, or hyphen." 2
continue
fi
stack_dir=""
if create_stack_directory_with_metadata stack_dir "${stack_name}"; then
handle_stack_topology_flow "${stack_dir}"
topology_status=$?
case "${topology_status}" in
"${FLOW_BACK_TO_MAIN}")
return "${FLOW_BACK_TO_MAIN}"
;;
"${FLOW_EXIT_APP}")
return "${FLOW_EXIT_APP}"
;;
*)
return "${FLOW_CONTINUE}"
;;
esac
else
create_stack_status=$?
if [ "${create_stack_status}" -eq 2 ]; then
show_warning_and_wait "Stack already exists: ${stack_name}" 2
continue
fi
show_warning_and_wait "Could not create stack directory for: ${stack_name}" 2
return "${FLOW_CONTINUE}"
fi
done
}
handle_manage_existing_stacks_flow() {
local manage_action=""
manage_action="$(show_manage_stacks_placeholder || true)"
case "${manage_action}" in
"Back to production setup")
return "${FLOW_CONTINUE}"
;;
"Back to main menu" | "")
return "${FLOW_BACK_TO_MAIN}"
;;
"Exit and close easy-docker")
return "${FLOW_EXIT_APP}"
;;
*)
show_warning_and_wait "Unknown manage-stacks action: ${manage_action}"
return "${FLOW_CONTINUE}"
;;
esac
}
handle_production_setup_flow() {
local production_action=""
while true; do
production_action="$(show_production_setup_menu || true)"
case "${production_action}" in
"Create new stack")
if handle_create_new_stack_flow; then
:
else
case "$?" in
"${FLOW_BACK_TO_MAIN}")
return "${FLOW_BACK_TO_MAIN}"
;;
"${FLOW_EXIT_APP}")
return "${FLOW_EXIT_APP}"
;;
*) ;;
esac
fi
;;
"Manage existing stacks")
if handle_manage_existing_stacks_flow; then
:
else
case "$?" in
"${FLOW_BACK_TO_MAIN}")
return "${FLOW_BACK_TO_MAIN}"
;;
"${FLOW_EXIT_APP}")
return "${FLOW_EXIT_APP}"
;;
*) ;;
esac
fi
;;
"Back to main menu" | "")
return "${FLOW_BACK_TO_MAIN}"
;;
"Exit and close easy-docker")
return "${FLOW_EXIT_APP}"
;;
*)
show_warning_and_wait "Unknown production action: ${production_action}"
;;
esac
done
}
handle_environment_check_flow() {
local environment_action=""
environment_action="$(show_environment_status || true)"
case "${environment_action}" in
"Back to main menu" | "")
return "${FLOW_BACK_TO_MAIN}"
;;
"Exit and close easy-docker")
return "${FLOW_EXIT_APP}"
;;
*)
show_warning_and_wait "Unknown environment action: ${environment_action}"
return "${FLOW_CONTINUE}"
;;
esac
}
run_easy_docker_app() {
local action=""
local local_env_action=""
local local_production_action=""
local local_production_sub_action=""
local stack_name=""
local stack_env_path=""
local create_stack_status=0
local stack_input_status=0
local handler_status=0
enter_alt_screen
render_main_screen 1
while true; do
local_env_action=""
local_production_action=""
local_production_sub_action=""
action="$(show_main_menu || true)"
if [ -z "${action}" ]; then
@ -98,116 +392,42 @@ run_easy_docker_app() {
case "${action}" in
"Production setup")
while true; do
local_production_action="$(show_production_setup_menu || true)"
case "${local_production_action}" in
"Create new stack")
while true; do
stack_name=""
if ! prompt_stack_name_with_cancel stack_name; then
stack_input_status=$?
if [ "${stack_input_status}" -eq 3 ]; then
break
fi
show_warning_message "Input canceled."
sleep 1
break
fi
if [ -z "${stack_name}" ]; then
break
fi
if ! is_valid_stack_name "${stack_name}"; then
show_warning_message "Invalid stack name. Use letters, numbers, dot, underscore, or hyphen."
sleep 2
continue
fi
stack_env_path=""
if create_stack_env_file stack_env_path "${stack_name}"; then
local_production_sub_action="$(show_create_stack_created "${stack_name}" "${stack_env_path}" || true)"
else
create_stack_status=$?
if [ "${create_stack_status}" -eq 2 ]; then
show_warning_message "Stack already exists: ${stack_name}"
sleep 2
continue
else
show_warning_message "Could not create stack env file for: ${stack_name}"
sleep 2
break
fi
fi
case "${local_production_sub_action}" in
"Continue stack wizard")
show_warning_message "Next wizard step is coming soon."
sleep 2
;;
"Back to production setup" | "") ;;
*)
show_warning_message "Unknown create-stack action: ${local_production_sub_action}"
sleep 1
;;
esac
break
done
;;
"Manage existing stacks")
local_production_sub_action="$(show_manage_stacks_placeholder || true)"
case "${local_production_sub_action}" in
"Back to production setup") ;;
"Back to main menu" | "")
render_main_screen 1
break
;;
"Exit and close easy-docker")
return 0
;;
*)
show_warning_message "Unknown manage-stacks action: ${local_production_sub_action}"
sleep 1
;;
esac
;;
"Back to main menu" | "")
render_main_screen 1
break
;;
"Exit and close easy-docker")
return 0
;;
*)
show_warning_message "Unknown production action: ${local_production_action}"
sleep 1
;;
esac
done
;;
"Environment check")
local_env_action="$(show_environment_status || true)"
case "${local_env_action}" in
"Back to main menu" | "")
if handle_production_setup_flow; then
handler_status="${FLOW_CONTINUE}"
else
handler_status=$?
fi
case "${handler_status}" in
"${FLOW_BACK_TO_MAIN}")
render_main_screen 1
;;
"Exit and close easy-docker")
"${FLOW_EXIT_APP}")
return 0
;;
*)
show_warning_message "Unknown environment action: ${local_env_action}"
sleep 1
*) ;;
esac
;;
"Environment check")
if handle_environment_check_flow; then
handler_status="${FLOW_CONTINUE}"
else
handler_status=$?
fi
case "${handler_status}" in
"${FLOW_BACK_TO_MAIN}")
render_main_screen 1
;;
"${FLOW_EXIT_APP}")
return 0
;;
*) ;;
esac
;;
"Exit")
return 0
;;
*)
show_warning_message "Unknown action: ${action}"
sleep 1
show_warning_and_wait "Unknown action: ${action}"
;;
esac
done

25
scripts/easy-docker/lib/load.sh Executable file
View file

@ -0,0 +1,25 @@
#!/usr/bin/env bash
load_easy_docker_modules() {
local lib_dir=""
lib_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=scripts/easy-docker/lib/core/commands.sh
source "${lib_dir}/core/commands.sh"
# shellcheck source=scripts/easy-docker/lib/core/messages.sh
source "${lib_dir}/core/messages.sh"
# shellcheck source=scripts/easy-docker/lib/install/gum/load.sh
source "${lib_dir}/install/gum/load.sh"
# shellcheck source=scripts/easy-docker/lib/checks/docker.sh
source "${lib_dir}/checks/docker.sh"
# shellcheck source=scripts/easy-docker/lib/ui/screens.sh
source "${lib_dir}/ui/screens.sh"
# shellcheck source=scripts/easy-docker/lib/app/screen.sh
source "${lib_dir}/app/screen.sh"
# shellcheck source=scripts/easy-docker/lib/app/options.sh
source "${lib_dir}/app/options.sh"
# shellcheck source=scripts/easy-docker/lib/app/run.sh
source "${lib_dir}/app/run.sh"
}
load_easy_docker_modules

View file

@ -35,24 +35,96 @@ prompt_new_stack_name() {
--placeholder "my-production-stack"
}
show_create_stack_created() {
local stack_name="${1}"
local env_path="${2}"
show_stack_topology_menu() {
local stack_dir="${1}"
local stack_name=""
local status_text=""
render_main_screen 1 >&2
status_text="$(printf "Create new stack\n\nStack created: %s\nEnv file: %s" "${stack_name}" "${env_path}")"
stack_name="${stack_dir##*/}"
status_text="$(printf "Stack created: %s\nDirectory: %s\n\nChoose the deployment topology." "${stack_name}" "${stack_dir}")"
render_box_message "${status_text}" "0 2" >&2
gum choose \
--height 6 \
--header "Create stack actions" \
--height 9 \
--header "Topology" \
--cursor.foreground 63 \
--selected.foreground 45 \
"Continue stack wizard" \
"Back to production setup"
"Single-host" \
"Split services" \
"Advanced" \
"Abort wizard to main menu"
}
show_single_host_examples() {
local status_text=""
render_main_screen 1 >&2
status_text="$(printf "Single-host examples\n\n- One server, one compose project.\n- Local DB/Redis/Proxy with app services together.\n- Typical small production VM setup.")"
render_box_message "${status_text}" "0 2" >&2
gum choose \
--height 7 \
--header "Single-host" \
--cursor.foreground 63 \
--selected.foreground 45 \
"Use this topology" \
"Back to topology selection"
}
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_advanced_examples() {
local status_text=""
render_main_screen 1 >&2
status_text="$(printf "Advanced examples\n\n- Managed external DB/Redis.\n- Multiple benches with custom images/tags.\n- GitOps-style rendered compose and custom networks/secrets.")"
render_box_message "${status_text}" "0 2" >&2
gum choose \
--height 7 \
--header "Advanced" \
--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_placeholder() {

View file

@ -3,22 +3,8 @@ set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=scripts/easy-docker/lib/core/commands.sh
source "${SCRIPT_DIR}/lib/core/commands.sh"
# shellcheck source=scripts/easy-docker/lib/core/messages.sh
source "${SCRIPT_DIR}/lib/core/messages.sh"
# shellcheck source=scripts/easy-docker/lib/install/gum/load.sh
source "${SCRIPT_DIR}/lib/install/gum/load.sh"
# shellcheck source=scripts/easy-docker/lib/checks/docker.sh
source "${SCRIPT_DIR}/lib/checks/docker.sh"
# shellcheck source=scripts/easy-docker/lib/ui/screens.sh
source "${SCRIPT_DIR}/lib/ui/screens.sh"
# shellcheck source=scripts/easy-docker/lib/app/screen.sh
source "${SCRIPT_DIR}/lib/app/screen.sh"
# shellcheck source=scripts/easy-docker/lib/app/options.sh
source "${SCRIPT_DIR}/lib/app/options.sh"
# shellcheck source=scripts/easy-docker/lib/app/run.sh
source "${SCRIPT_DIR}/lib/app/run.sh"
# shellcheck source=scripts/easy-docker/lib/load.sh
source "${SCRIPT_DIR}/lib/load.sh"
disable_installation_fallback=0
if parse_cli_options disable_installation_fallback "$@"; then