feat(easy-docker): add split-services stack workflow

This commit is contained in:
RocketQuack 2026-04-14 11:22:14 +02:00
parent 1594dc701e
commit 61890d9446
24 changed files with 969 additions and 119 deletions

View file

@ -20,6 +20,8 @@ load_easy_docker_wizard_common_modules() {
source "${wizard_dir}/common/site.sh"
# shellcheck source=scripts/easy-docker/lib/app/wizard/common/ux.sh
source "${wizard_dir}/common/ux.sh"
# shellcheck source=scripts/easy-docker/lib/app/wizard/split_services.sh
source "${wizard_dir}/split_services.sh"
}
load_easy_docker_wizard_common_modules

View file

@ -27,7 +27,7 @@ start_stack_with_compose_from_metadata() {
return 32
fi
if ! easy_docker_compose_require_single_host_topology "${stack_dir}" 33 34; then
if ! easy_docker_compose_require_supported_topology "${stack_dir}" 33 34; then
return $?
fi
@ -105,7 +105,7 @@ stop_stack_with_compose_from_metadata() {
return 42
fi
if ! easy_docker_compose_require_single_host_topology "${stack_dir}" 43 44; then
if ! easy_docker_compose_require_supported_topology "${stack_dir}" 43 44; then
return $?
fi
@ -150,7 +150,7 @@ delete_stack_with_compose_from_metadata() {
return 49
fi
if ! easy_docker_compose_require_single_host_topology "${stack_dir}" 50 51; then
if ! easy_docker_compose_require_supported_topology "${stack_dir}" 50 51; then
return $?
fi

View file

@ -32,7 +32,7 @@ easy_docker_compose_get_fallback_erpnext_version() {
printf -v "${result_var}" "%s" "${fallback_erpnext_version}"
}
easy_docker_compose_require_single_host_topology() {
easy_docker_compose_require_supported_topology() {
local stack_dir="${1}"
local missing_topology_code="${2}"
local unsupported_topology_code="${3}"
@ -46,7 +46,7 @@ easy_docker_compose_require_single_host_topology() {
fi
case "${stack_topology}" in
"single-host")
"single-host" | "split-services")
return 0
;;
*)

View file

@ -48,9 +48,9 @@ get_stack_compose_runtime_status_label() {
fi
case "${stack_topology}" in
"single-host") ;;
"single-host" | "split-services") ;;
*)
printf -v "${result_var}" "%s" "N/A (${stack_topology})"
printf -v "${result_var}" "%s" "Unsupported (${stack_topology})"
return 0
;;
esac

View file

@ -61,7 +61,7 @@ delete_stack_with_compose_from_metadata() {
fi
case "${stack_topology}" in
"single-host") ;;
"single-host" | "split-services") ;;
*)
EASY_DOCKER_COMPOSE_ERROR_DETAIL="${stack_topology}"
return 51

View file

@ -43,7 +43,7 @@ start_stack_with_compose_from_metadata() {
fi
case "${stack_topology}" in
"single-host") ;;
"single-host" | "split-services") ;;
*)
# shellcheck disable=SC2034 # Read by manage flow after start_stack_with_compose_from_metadata returns 34.
EASY_DOCKER_COMPOSE_ERROR_DETAIL="${stack_topology}"

View file

@ -51,9 +51,9 @@ get_stack_compose_runtime_status_label() {
fi
case "${stack_topology}" in
"single-host") ;;
"single-host" | "split-services") ;;
*)
printf -v "${result_var}" "%s" "N/A (${stack_topology})"
printf -v "${result_var}" "%s" "Unsupported (${stack_topology})"
return 0
;;
esac

View file

@ -37,7 +37,7 @@ stop_stack_with_compose_from_metadata() {
fi
case "${stack_topology}" in
"single-host") ;;
"single-host" | "split-services") ;;
*)
# shellcheck disable=SC2034 # Read by manage flow after stop_stack_with_compose_from_metadata returns 44.
EASY_DOCKER_COMPOSE_ERROR_DETAIL="${stack_topology}"

View file

@ -34,7 +34,7 @@ run_stack_backend_bash_command() {
fi
case "${stack_topology}" in
single-host) ;;
single-host | split-services) ;;
*)
return 52
;;

View file

@ -12,6 +12,8 @@ load_easy_docker_wizard_env_modules() {
source "${wizard_dir}/env/update.sh"
# shellcheck source=scripts/easy-docker/lib/app/wizard/env/collect.sh
source "${wizard_dir}/env/collect.sh"
# shellcheck source=scripts/easy-docker/lib/app/wizard/env/split_services.sh
source "${wizard_dir}/env/split_services.sh"
}
load_easy_docker_wizard_env_modules

View file

@ -249,7 +249,7 @@ prompt_custom_modular_apps_data() {
local existing_branch_lines=""
local selected_branch_lines=""
local selected_app_count=0
local built_apps_metadata_json_object=""
local assembled_apps_metadata_json_object=""
local -a predefined_catalog_entries=()
local -a selected_predefined_ids=()
@ -393,8 +393,8 @@ EOF
continue
fi
build_predefined_apps_metadata_json_object built_apps_metadata_json_object "${selected_predefined_csv}" "${selected_branch_lines}"
printf -v "${result_apps_metadata_var}" "%s" "${built_apps_metadata_json_object}"
build_predefined_apps_metadata_json_object assembled_apps_metadata_json_object "${selected_predefined_csv}" "${selected_branch_lines}"
printf -v "${result_apps_metadata_var}" "%s" "${assembled_apps_metadata_json_object}"
return 0
done
}
@ -446,7 +446,7 @@ prompt_selected_stack_app_branches_data() {
local existing_branch_lines=""
local selected_branch_lines=""
local selected_app_count=0
local built_apps_metadata_json_object=""
local assembled_apps_metadata_json_object=""
local prompt_status=0
local -a selected_predefined_ids=()
@ -515,8 +515,8 @@ prompt_selected_stack_app_branches_data() {
return 4
fi
build_predefined_apps_metadata_json_object built_apps_metadata_json_object "${selected_predefined_csv}" "${selected_branch_lines}"
printf -v "${result_apps_metadata_var}" "%s" "${built_apps_metadata_json_object}"
build_predefined_apps_metadata_json_object assembled_apps_metadata_json_object "${selected_predefined_csv}" "${selected_branch_lines}"
printf -v "${result_apps_metadata_var}" "%s" "${assembled_apps_metadata_json_object}"
return 0
}

View file

@ -1,21 +1,13 @@
#!/usr/bin/env bash
collect_single_host_env_lines() {
collect_stack_image_and_apps_env_lines() {
local result_env_var="${1}"
local result_apps_metadata_var="${2}"
local stack_dir="${3}"
local proxy_mode_id="${4}"
local database_id="${5}"
local collected_env_lines=""
local value=""
local domains_value=""
local domain_lines=""
local site_domains_value=""
local built_env_lines=""
local custom_image_value=""
local custom_tag_value=""
local selected_apps_metadata_json_object=""
local sites_rule_value=""
local nginx_proxy_hosts_value=""
local built_apps_metadata_json_object=""
local prompt_status=0
if prompt_env_value_with_validation custom_image_value "${stack_dir}" "CUSTOM_IMAGE" "Required for custom modular image mode.\nExample: ghcr.io/acme/frappe-custom\nType /back to return." "ghcr.io/acme/frappe-custom" "required" "none"; then
@ -24,7 +16,7 @@ collect_single_host_env_lines() {
prompt_status=$?
return "${prompt_status}"
fi
collected_env_lines="$(append_env_line "${collected_env_lines}" "CUSTOM_IMAGE" "${custom_image_value}")"
built_env_lines="$(append_env_line "${built_env_lines}" "CUSTOM_IMAGE" "${custom_image_value}")"
if prompt_env_value_with_validation custom_tag_value "${stack_dir}" "CUSTOM_TAG" "Required for custom modular image mode.\nExample: v1.0.0\nType /back to return." "v1.0.0" "required" "none"; then
:
@ -32,19 +24,48 @@ collect_single_host_env_lines() {
prompt_status=$?
return "${prompt_status}"
fi
collected_env_lines="$(append_env_line "${collected_env_lines}" "CUSTOM_TAG" "${custom_tag_value}")"
built_env_lines="$(append_env_line "${built_env_lines}" "CUSTOM_TAG" "${custom_tag_value}")"
if prompt_custom_modular_apps_data selected_apps_metadata_json_object "${stack_dir}"; then
if prompt_custom_modular_apps_data built_apps_metadata_json_object "${stack_dir}"; then
:
else
prompt_status=$?
return "${prompt_status}"
fi
if [ -z "${selected_apps_metadata_json_object}" ]; then
if [ -z "${built_apps_metadata_json_object}" ]; then
return 1
fi
printf -v "${result_env_var}" "%s" "${built_env_lines}"
printf -v "${result_apps_metadata_var}" "%s" "${built_apps_metadata_json_object}"
return 0
}
collect_single_host_env_lines() {
local result_env_var="${1}"
local result_apps_metadata_var="${2}"
local stack_dir="${3}"
local proxy_mode_id="${4}"
local database_id="${5}"
local redis_id="${6}"
local collected_single_host_env_lines=""
local collected_single_host_apps_metadata_json_object=""
local value=""
local domains_value=""
local domain_lines=""
local site_domains_value=""
local sites_rule_value=""
local nginx_proxy_hosts_value=""
local prompt_status=0
if collect_stack_image_and_apps_env_lines collected_single_host_env_lines collected_single_host_apps_metadata_json_object "${stack_dir}"; then
:
else
prompt_status=$?
return "${prompt_status}"
fi
case "${proxy_mode_id}" in
traefik-https)
if prompt_env_value_with_validation domains_value "${stack_dir}" "SITE_DOMAINS" "Required for Traefik HTTPS routing.\nUse hostnames like example.com, app.example.com, localhost, or dev.localhost.\nEnter multiple domains separated by comma or space.\nLet's Encrypt still requires a public DNS name.\nType /back to return." "erp.example.com dev.localhost" "required" "domains"; then
@ -60,10 +81,10 @@ collect_single_host_env_lines() {
fi
site_domains_value="$(domain_lines_to_csv "${domain_lines}")"
collected_env_lines="$(append_env_line "${collected_env_lines}" "SITE_DOMAINS" "${site_domains_value}")"
collected_single_host_env_lines="$(append_env_line "${collected_single_host_env_lines}" "SITE_DOMAINS" "${site_domains_value}")"
sites_rule_value="$(domain_lines_to_sites_rule "${domain_lines}")"
collected_env_lines="$(append_env_line "${collected_env_lines}" "SITES_RULE" "${sites_rule_value}")"
collected_single_host_env_lines="$(append_env_line "${collected_single_host_env_lines}" "SITES_RULE" "${sites_rule_value}")"
if prompt_env_value_with_validation value "${stack_dir}" "LETSENCRYPT_EMAIL" "Required for Let's Encrypt certificate registration.\nType /back to return." "admin@example.com" "required" "email"; then
:
@ -71,7 +92,7 @@ collect_single_host_env_lines() {
prompt_status=$?
return "${prompt_status}"
fi
collected_env_lines="$(append_env_line "${collected_env_lines}" "LETSENCRYPT_EMAIL" "${value}")"
collected_single_host_env_lines="$(append_env_line "${collected_single_host_env_lines}" "LETSENCRYPT_EMAIL" "${value}")"
if prompt_env_value_with_validation value "${stack_dir}" "HTTP_PUBLISH_PORT" "Optional. Press Enter to keep default 80.\nType /back to return." "80" "optional" "port"; then
:
@ -80,7 +101,7 @@ collect_single_host_env_lines() {
return "${prompt_status}"
fi
if [ -n "${value}" ]; then
collected_env_lines="$(append_env_line "${collected_env_lines}" "HTTP_PUBLISH_PORT" "${value}")"
collected_single_host_env_lines="$(append_env_line "${collected_single_host_env_lines}" "HTTP_PUBLISH_PORT" "${value}")"
fi
if prompt_env_value_with_validation value "${stack_dir}" "HTTPS_PUBLISH_PORT" "Optional. Press Enter to keep default 443.\nType /back to return." "443" "optional" "port"; then
@ -90,7 +111,7 @@ collect_single_host_env_lines() {
return "${prompt_status}"
fi
if [ -n "${value}" ]; then
collected_env_lines="$(append_env_line "${collected_env_lines}" "HTTPS_PUBLISH_PORT" "${value}")"
collected_single_host_env_lines="$(append_env_line "${collected_single_host_env_lines}" "HTTPS_PUBLISH_PORT" "${value}")"
fi
;;
nginxproxy-https)
@ -107,10 +128,10 @@ collect_single_host_env_lines() {
fi
site_domains_value="$(domain_lines_to_csv "${domain_lines}")"
collected_env_lines="$(append_env_line "${collected_env_lines}" "SITE_DOMAINS" "${site_domains_value}")"
collected_single_host_env_lines="$(append_env_line "${collected_single_host_env_lines}" "SITE_DOMAINS" "${site_domains_value}")"
nginx_proxy_hosts_value="$(domain_lines_to_csv "${domain_lines}")"
collected_env_lines="$(append_env_line "${collected_env_lines}" "NGINX_PROXY_HOSTS" "${nginx_proxy_hosts_value}")"
collected_single_host_env_lines="$(append_env_line "${collected_single_host_env_lines}" "NGINX_PROXY_HOSTS" "${nginx_proxy_hosts_value}")"
if prompt_env_value_with_validation value "${stack_dir}" "LETSENCRYPT_EMAIL" "Required for Let's Encrypt certificate registration.\nType /back to return." "admin@example.com" "required" "email"; then
:
@ -118,7 +139,7 @@ collect_single_host_env_lines() {
prompt_status=$?
return "${prompt_status}"
fi
collected_env_lines="$(append_env_line "${collected_env_lines}" "LETSENCRYPT_EMAIL" "${value}")"
collected_single_host_env_lines="$(append_env_line "${collected_single_host_env_lines}" "LETSENCRYPT_EMAIL" "${value}")"
if prompt_env_value_with_validation value "${stack_dir}" "HTTP_PUBLISH_PORT" "Optional. Press Enter to keep default 80.\nType /back to return." "80" "optional" "port"; then
:
@ -127,7 +148,7 @@ collect_single_host_env_lines() {
return "${prompt_status}"
fi
if [ -n "${value}" ]; then
collected_env_lines="$(append_env_line "${collected_env_lines}" "HTTP_PUBLISH_PORT" "${value}")"
collected_single_host_env_lines="$(append_env_line "${collected_single_host_env_lines}" "HTTP_PUBLISH_PORT" "${value}")"
fi
if prompt_env_value_with_validation value "${stack_dir}" "HTTPS_PUBLISH_PORT" "Optional. Press Enter to keep default 443.\nType /back to return." "443" "optional" "port"; then
@ -137,7 +158,7 @@ collect_single_host_env_lines() {
return "${prompt_status}"
fi
if [ -n "${value}" ]; then
collected_env_lines="$(append_env_line "${collected_env_lines}" "HTTPS_PUBLISH_PORT" "${value}")"
collected_single_host_env_lines="$(append_env_line "${collected_single_host_env_lines}" "HTTPS_PUBLISH_PORT" "${value}")"
fi
;;
nginxproxy-http)
@ -154,10 +175,10 @@ collect_single_host_env_lines() {
fi
site_domains_value="$(domain_lines_to_csv "${domain_lines}")"
collected_env_lines="$(append_env_line "${collected_env_lines}" "SITE_DOMAINS" "${site_domains_value}")"
collected_single_host_env_lines="$(append_env_line "${collected_single_host_env_lines}" "SITE_DOMAINS" "${site_domains_value}")"
nginx_proxy_hosts_value="$(domain_lines_to_csv "${domain_lines}")"
collected_env_lines="$(append_env_line "${collected_env_lines}" "NGINX_PROXY_HOSTS" "${nginx_proxy_hosts_value}")"
collected_single_host_env_lines="$(append_env_line "${collected_single_host_env_lines}" "NGINX_PROXY_HOSTS" "${nginx_proxy_hosts_value}")"
if prompt_env_value_with_validation value "${stack_dir}" "HTTP_PUBLISH_PORT" "Optional. Press Enter to keep default 80.\nType /back to return." "80" "optional" "port"; then
:
@ -166,7 +187,7 @@ collect_single_host_env_lines() {
return "${prompt_status}"
fi
if [ -n "${value}" ]; then
collected_env_lines="$(append_env_line "${collected_env_lines}" "HTTP_PUBLISH_PORT" "${value}")"
collected_single_host_env_lines="$(append_env_line "${collected_single_host_env_lines}" "HTTP_PUBLISH_PORT" "${value}")"
fi
;;
traefik-http)
@ -177,7 +198,7 @@ collect_single_host_env_lines() {
return "${prompt_status}"
fi
if [ -n "${value}" ]; then
collected_env_lines="$(append_env_line "${collected_env_lines}" "HTTP_PUBLISH_PORT" "${value}")"
collected_single_host_env_lines="$(append_env_line "${collected_single_host_env_lines}" "HTTP_PUBLISH_PORT" "${value}")"
fi
;;
caddy-external | no-proxy)
@ -188,7 +209,7 @@ collect_single_host_env_lines() {
return "${prompt_status}"
fi
if [ -n "${value}" ]; then
collected_env_lines="$(append_env_line "${collected_env_lines}" "HTTP_PUBLISH_PORT" "${value}")"
collected_single_host_env_lines="$(append_env_line "${collected_single_host_env_lines}" "HTTP_PUBLISH_PORT" "${value}")"
fi
;;
*)
@ -205,7 +226,7 @@ collect_single_host_env_lines() {
prompt_status=$?
return "${prompt_status}"
fi
collected_env_lines="$(append_env_line "${collected_env_lines}" "DB_PASSWORD" "${value}")"
collected_single_host_env_lines="$(append_env_line "${collected_single_host_env_lines}" "DB_PASSWORD" "${value}")"
;;
mariadb)
if prompt_env_value_with_validation value "${stack_dir}" "DB_PASSWORD" "Optional but recommended for MariaDB.\nPress Enter to use default from override.\nType /back to return." "changeit" "optional" "none"; then
@ -215,7 +236,7 @@ collect_single_host_env_lines() {
return "${prompt_status}"
fi
if [ -n "${value}" ]; then
collected_env_lines="$(append_env_line "${collected_env_lines}" "DB_PASSWORD" "${value}")"
collected_single_host_env_lines="$(append_env_line "${collected_single_host_env_lines}" "DB_PASSWORD" "${value}")"
fi
;;
*)
@ -224,7 +245,21 @@ collect_single_host_env_lines() {
;;
esac
printf -v "${result_env_var}" "%s" "${collected_env_lines}"
printf -v "${result_apps_metadata_var}" "%s" "${selected_apps_metadata_json_object}"
case "${redis_id}" in
enabled)
collected_single_host_env_lines="$(append_env_line "${collected_single_host_env_lines}" "REDIS_CACHE" "redis-cache:6379")"
collected_single_host_env_lines="$(append_env_line "${collected_single_host_env_lines}" "REDIS_QUEUE" "redis-queue:6379")"
;;
disabled | "")
:
;;
*)
show_warning_and_wait "Unknown Redis id: ${redis_id}" 2
return 1
;;
esac
printf -v "${result_env_var}" "%s" "${collected_single_host_env_lines}"
printf -v "${result_apps_metadata_var}" "%s" "${collected_single_host_apps_metadata_json_object}"
return 0
}

View file

@ -0,0 +1,319 @@
#!/usr/bin/env bash
collect_split_services_env_lines() {
local result_env_var="${1}"
local result_apps_metadata_var="${2}"
local stack_dir="${3}"
local proxy_mode_id="${4}"
local data_mode_id="${5}"
local database_id="${6}"
local redis_id="${7}"
local collected_split_services_env_lines=""
local collected_split_services_apps_metadata_json_object=""
local value=""
local domains_value=""
local domain_lines=""
local site_domains_value=""
local sites_rule_value=""
local nginx_proxy_hosts_value=""
local db_port=""
local prompt_status=0
if collect_stack_image_and_apps_env_lines collected_split_services_env_lines collected_split_services_apps_metadata_json_object "${stack_dir}"; then
:
else
prompt_status=$?
return "${prompt_status}"
fi
case "${proxy_mode_id}" in
traefik-https)
if prompt_env_value_with_validation domains_value "${stack_dir}" "SITE_DOMAINS" "Required for Traefik HTTPS routing.\nUse hostnames like example.com, app.example.com, localhost, or dev.localhost.\nEnter multiple domains separated by comma or space.\nLet's Encrypt still requires a public DNS name.\nType /back to return." "erp.example.com dev.localhost" "required" "domains"; then
:
else
prompt_status=$?
return "${prompt_status}"
fi
if ! parse_domains_input_to_lines domain_lines "${domains_value}"; then
show_warning_message "Could not parse SITE_DOMAINS."
return 1
fi
site_domains_value="$(domain_lines_to_csv "${domain_lines}")"
collected_split_services_env_lines="$(append_env_line "${collected_split_services_env_lines}" "SITE_DOMAINS" "${site_domains_value}")"
sites_rule_value="$(domain_lines_to_sites_rule "${domain_lines}")"
collected_split_services_env_lines="$(append_env_line "${collected_split_services_env_lines}" "SITES_RULE" "${sites_rule_value}")"
if prompt_env_value_with_validation value "${stack_dir}" "LETSENCRYPT_EMAIL" "Required for Let's Encrypt certificate registration.\nType /back to return." "admin@example.com" "required" "email"; then
:
else
prompt_status=$?
return "${prompt_status}"
fi
collected_split_services_env_lines="$(append_env_line "${collected_split_services_env_lines}" "LETSENCRYPT_EMAIL" "${value}")"
if prompt_env_value_with_validation value "${stack_dir}" "HTTP_PUBLISH_PORT" "Optional. Press Enter to keep default 80.\nType /back to return." "80" "optional" "port"; then
:
else
prompt_status=$?
return "${prompt_status}"
fi
if [ -n "${value}" ]; then
collected_split_services_env_lines="$(append_env_line "${collected_split_services_env_lines}" "HTTP_PUBLISH_PORT" "${value}")"
fi
if prompt_env_value_with_validation value "${stack_dir}" "HTTPS_PUBLISH_PORT" "Optional. Press Enter to keep default 443.\nType /back to return." "443" "optional" "port"; then
:
else
prompt_status=$?
return "${prompt_status}"
fi
if [ -n "${value}" ]; then
collected_split_services_env_lines="$(append_env_line "${collected_split_services_env_lines}" "HTTPS_PUBLISH_PORT" "${value}")"
fi
;;
nginxproxy-https)
if prompt_env_value_with_validation domains_value "${stack_dir}" "SITE_DOMAINS" "Required for nginx-proxy routing.\nUse hostnames like example.com, app.example.com, localhost, or dev.localhost.\nEnter multiple domains separated by comma or space.\nLet's Encrypt still requires a public DNS name.\nType /back to return." "erp.example.com dev.localhost" "required" "domains"; then
:
else
prompt_status=$?
return "${prompt_status}"
fi
if ! parse_domains_input_to_lines domain_lines "${domains_value}"; then
show_warning_message "Could not parse SITE_DOMAINS."
return 1
fi
site_domains_value="$(domain_lines_to_csv "${domain_lines}")"
collected_split_services_env_lines="$(append_env_line "${collected_split_services_env_lines}" "SITE_DOMAINS" "${site_domains_value}")"
nginx_proxy_hosts_value="$(domain_lines_to_csv "${domain_lines}")"
collected_split_services_env_lines="$(append_env_line "${collected_split_services_env_lines}" "NGINX_PROXY_HOSTS" "${nginx_proxy_hosts_value}")"
if prompt_env_value_with_validation value "${stack_dir}" "LETSENCRYPT_EMAIL" "Required for Let's Encrypt certificate registration.\nType /back to return." "admin@example.com" "required" "email"; then
:
else
prompt_status=$?
return "${prompt_status}"
fi
collected_split_services_env_lines="$(append_env_line "${collected_split_services_env_lines}" "LETSENCRYPT_EMAIL" "${value}")"
if prompt_env_value_with_validation value "${stack_dir}" "HTTP_PUBLISH_PORT" "Optional. Press Enter to keep default 80.\nType /back to return." "80" "optional" "port"; then
:
else
prompt_status=$?
return "${prompt_status}"
fi
if [ -n "${value}" ]; then
collected_split_services_env_lines="$(append_env_line "${collected_split_services_env_lines}" "HTTP_PUBLISH_PORT" "${value}")"
fi
if prompt_env_value_with_validation value "${stack_dir}" "HTTPS_PUBLISH_PORT" "Optional. Press Enter to keep default 443.\nType /back to return." "443" "optional" "port"; then
:
else
prompt_status=$?
return "${prompt_status}"
fi
if [ -n "${value}" ]; then
collected_split_services_env_lines="$(append_env_line "${collected_split_services_env_lines}" "HTTPS_PUBLISH_PORT" "${value}")"
fi
;;
nginxproxy-http)
if prompt_env_value_with_validation domains_value "${stack_dir}" "SITE_DOMAINS" "Required for nginx-proxy routing.\nUse hostnames like example.com, app.example.com, localhost, or dev.localhost.\nEnter multiple domains separated by comma or space.\nType /back to return." "erp.example.com dev.localhost" "required" "domains"; then
:
else
prompt_status=$?
return "${prompt_status}"
fi
if ! parse_domains_input_to_lines domain_lines "${domains_value}"; then
show_warning_message "Could not parse SITE_DOMAINS."
return 1
fi
site_domains_value="$(domain_lines_to_csv "${domain_lines}")"
collected_split_services_env_lines="$(append_env_line "${collected_split_services_env_lines}" "SITE_DOMAINS" "${site_domains_value}")"
nginx_proxy_hosts_value="$(domain_lines_to_csv "${domain_lines}")"
collected_split_services_env_lines="$(append_env_line "${collected_split_services_env_lines}" "NGINX_PROXY_HOSTS" "${nginx_proxy_hosts_value}")"
if prompt_env_value_with_validation value "${stack_dir}" "HTTP_PUBLISH_PORT" "Optional. Press Enter to keep default 80.\nType /back to return." "80" "optional" "port"; then
:
else
prompt_status=$?
return "${prompt_status}"
fi
if [ -n "${value}" ]; then
collected_split_services_env_lines="$(append_env_line "${collected_split_services_env_lines}" "HTTP_PUBLISH_PORT" "${value}")"
fi
;;
traefik-http)
if prompt_env_value_with_validation value "${stack_dir}" "HTTP_PUBLISH_PORT" "Optional. Press Enter to keep default 80.\nType /back to return." "80" "optional" "port"; then
:
else
prompt_status=$?
return "${prompt_status}"
fi
if [ -n "${value}" ]; then
collected_split_services_env_lines="$(append_env_line "${collected_split_services_env_lines}" "HTTP_PUBLISH_PORT" "${value}")"
fi
;;
caddy-external | no-proxy)
if prompt_env_value_with_validation value "${stack_dir}" "HTTP_PUBLISH_PORT" "Optional. Press Enter to keep default 8080 for no-proxy frontend publishing.\nType /back to return." "8080" "optional" "port"; then
:
else
prompt_status=$?
return "${prompt_status}"
fi
if [ -n "${value}" ]; then
collected_split_services_env_lines="$(append_env_line "${collected_split_services_env_lines}" "HTTP_PUBLISH_PORT" "${value}")"
fi
;;
*)
show_warning_and_wait "Unknown proxy mode id: ${proxy_mode_id}" 2
return 1
;;
esac
case "${data_mode_id}" in
managed)
case "${database_id}" in
postgres)
if prompt_env_value_with_validation value "${stack_dir}" "DB_PASSWORD" "Required for PostgreSQL database service.\nType /back to return." "changeit" "required" "none"; then
:
else
prompt_status=$?
return "${prompt_status}"
fi
collected_split_services_env_lines="$(append_env_line "${collected_split_services_env_lines}" "DB_PASSWORD" "${value}")"
;;
mariadb)
if prompt_env_value_with_validation value "${stack_dir}" "DB_PASSWORD" "Optional but recommended for MariaDB.\nPress Enter to use default from override.\nType /back to return." "changeit" "optional" "none"; then
:
else
prompt_status=$?
return "${prompt_status}"
fi
if [ -n "${value}" ]; then
collected_split_services_env_lines="$(append_env_line "${collected_split_services_env_lines}" "DB_PASSWORD" "${value}")"
fi
;;
*)
show_warning_and_wait "Unknown database id: ${database_id}" 2
return 1
;;
esac
case "${redis_id}" in
managed)
collected_split_services_env_lines="$(append_env_line "${collected_split_services_env_lines}" "REDIS_CACHE" "redis-cache:6379")"
collected_split_services_env_lines="$(append_env_line "${collected_split_services_env_lines}" "REDIS_QUEUE" "redis-queue:6379")"
;;
external)
if prompt_env_value_with_validation value "${stack_dir}" "REDIS_CACHE" "Required for external Redis cache.\nUse host:port such as redis.example.internal:6379.\nType /back to return." "redis.example.internal:6379" "required" "hostport"; then
:
else
prompt_status=$?
return "${prompt_status}"
fi
collected_split_services_env_lines="$(append_env_line "${collected_split_services_env_lines}" "REDIS_CACHE" "${value}")"
if prompt_env_value_with_validation value "${stack_dir}" "REDIS_QUEUE" "Required for external Redis queue.\nUse host:port such as redis.example.internal:6379.\nType /back to return." "redis.example.internal:6379" "required" "hostport"; then
:
else
prompt_status=$?
return "${prompt_status}"
fi
collected_split_services_env_lines="$(append_env_line "${collected_split_services_env_lines}" "REDIS_QUEUE" "${value}")"
;;
disabled | "")
:
;;
*)
show_warning_and_wait "Unknown Redis id: ${redis_id}" 2
return 1
;;
esac
;;
external)
case "${database_id}" in
postgres)
db_port="5432"
;;
mariadb)
db_port="3306"
;;
*)
show_warning_and_wait "Unknown database id: ${database_id}" 2
return 1
;;
esac
if prompt_env_value_with_validation value "${stack_dir}" "DB_HOST" "Required for external database.\nUse a hostname or IP address.\nType /back to return." "db.example.internal" "required" "host"; then
:
else
prompt_status=$?
return "${prompt_status}"
fi
collected_split_services_env_lines="$(append_env_line "${collected_split_services_env_lines}" "DB_HOST" "${value}")"
if prompt_env_value_with_validation value "${stack_dir}" "DB_PORT" "Required for external database.\nPress Enter to keep the default port.\nType /back to return." "${db_port}" "required" "port"; then
:
else
prompt_status=$?
return "${prompt_status}"
fi
collected_split_services_env_lines="$(append_env_line "${collected_split_services_env_lines}" "DB_PORT" "${value}")"
if prompt_env_value_with_validation value "${stack_dir}" "DB_PASSWORD" "Required for external database access.\nType /back to return." "changeit" "required" "none"; then
:
else
prompt_status=$?
return "${prompt_status}"
fi
collected_split_services_env_lines="$(append_env_line "${collected_split_services_env_lines}" "DB_PASSWORD" "${value}")"
case "${redis_id}" in
managed)
collected_split_services_env_lines="$(append_env_line "${collected_split_services_env_lines}" "REDIS_CACHE" "redis-cache:6379")"
collected_split_services_env_lines="$(append_env_line "${collected_split_services_env_lines}" "REDIS_QUEUE" "redis-queue:6379")"
;;
external)
if prompt_env_value_with_validation value "${stack_dir}" "REDIS_CACHE" "Required for external Redis cache.\nUse host:port such as redis.example.internal:6379.\nType /back to return." "redis.example.internal:6379" "required" "hostport"; then
:
else
prompt_status=$?
return "${prompt_status}"
fi
collected_split_services_env_lines="$(append_env_line "${collected_split_services_env_lines}" "REDIS_CACHE" "${value}")"
if prompt_env_value_with_validation value "${stack_dir}" "REDIS_QUEUE" "Required for external Redis queue.\nUse host:port such as redis.example.internal:6379.\nType /back to return." "redis.example.internal:6379" "required" "hostport"; then
:
else
prompt_status=$?
return "${prompt_status}"
fi
collected_split_services_env_lines="$(append_env_line "${collected_split_services_env_lines}" "REDIS_QUEUE" "${value}")"
;;
disabled | "")
:
;;
*)
show_warning_and_wait "Unknown Redis id: ${redis_id}" 2
return 1
;;
esac
;;
*)
show_warning_and_wait "Unknown data mode id: ${data_mode_id}" 2
return 1
;;
esac
printf -v "${result_env_var}" "%s" "${collected_split_services_env_lines}"
printf -v "${result_apps_metadata_var}" "%s" "${collected_split_services_apps_metadata_json_object}"
return 0
}

View file

@ -27,6 +27,74 @@ is_valid_port_number() {
return 0
}
is_valid_host_value() {
local value="${1}"
if [ -z "${value}" ] || [ "${#value}" -gt 253 ]; then
return 1
fi
if printf '%s' "${value}" | grep -Eq '[[:space:]/,:;?!@#]'; then
return 1
fi
case "${value}" in
.* | *. | *..*)
return 1
;;
esac
case "${value}" in
[A-Za-z0-9]*) ;;
*)
return 1
;;
esac
case "${value}" in
*[A-Za-z0-9]) ;;
*)
return 1
;;
esac
return 0
}
is_valid_host_port_value() {
local value="${1}"
local host=""
local port=""
if [ -z "${value}" ]; then
return 1
fi
case "${value}" in
*:*)
host="${value%:*}"
port="${value##*:}"
;;
*)
return 1
;;
esac
if [ -z "${host}" ] || [ -z "${port}" ]; then
return 1
fi
if ! is_valid_host_value "${host}"; then
return 1
fi
if ! is_valid_port_number "${port}"; then
return 1
fi
return 0
}
EASY_DOCKER_LAST_INVALID_DOMAIN=""
reset_domain_validation_feedback() {
@ -407,6 +475,18 @@ prompt_env_value_with_validation() {
continue
fi
;;
host)
if ! is_valid_host_value "${normalized_value}"; then
validation_feedback="Invalid host for ${variable_name}. Use a hostname or IP address without spaces."
continue
fi
;;
hostport)
if ! is_valid_host_port_value "${normalized_value}"; then
validation_feedback="Invalid endpoint for ${variable_name}. Use host:port, for example redis.example.internal:6379."
continue
fi
;;
domains)
if ! is_valid_domains_value "${normalized_value}"; then
invalid_domain_input="${EASY_DOCKER_LAST_INVALID_DOMAIN}"

View file

@ -6,6 +6,8 @@ load_easy_docker_wizard_flow_modules() {
# shellcheck source=scripts/easy-docker/lib/app/wizard/flows/single_host.sh
source "${wizard_dir}/flows/single_host.sh"
# shellcheck source=scripts/easy-docker/lib/app/wizard/flows/split_services.sh
source "${wizard_dir}/flows/split_services.sh"
# shellcheck source=scripts/easy-docker/lib/app/wizard/flows/manage.sh
source "${wizard_dir}/flows/manage.sh"
# shellcheck source=scripts/easy-docker/lib/app/wizard/flows/navigation.sh

View file

@ -38,6 +38,7 @@ handle_stack_topology_flow() {
local topology_action=""
local abort_status=0
local single_host_status=0
local split_services_status=0
local manage_status=0
local stack_name=""
@ -76,7 +77,38 @@ handle_stack_topology_flow() {
esac
;;
"Split services")
handle_topology_examples_flow "${topology_action}"
if handle_split_services_stack_flow "${stack_dir}"; then
split_services_status="${FLOW_CONTINUE}"
else
split_services_status=$?
fi
case "${split_services_status}" in
"${FLOW_OPEN_MANAGE_STACK}")
stack_name="${stack_dir##*/}"
if handle_manage_selected_stack_flow "${stack_name}"; then
manage_status="${FLOW_CONTINUE}"
else
manage_status=$?
fi
case "${manage_status}" in
"${FLOW_EXIT_APP}")
return "${FLOW_EXIT_APP}"
;;
*)
return "${FLOW_CONTINUE}"
;;
esac
;;
"${FLOW_BACK_TO_MAIN}")
return "${FLOW_BACK_TO_MAIN}"
;;
"${FLOW_EXIT_APP}")
return "${FLOW_EXIT_APP}"
;;
*) ;;
esac
;;
"Abort wizard to main menu")
handle_abort_wizard_flow "${stack_dir}"

View file

@ -79,32 +79,3 @@ handle_single_host_stack_flow() {
show_warning_and_wait "Single-host selection saved in ${stack_dir}/metadata.json, ${stack_env_path}, and ${stack_apps_path}. Rendered compose: ${generated_compose_path}." 3
return "${FLOW_OPEN_MANAGE_STACK}"
}
handle_topology_examples_flow() {
local topology_name="${1}"
local detail_action=""
case "${topology_name}" in
"Split services")
detail_action="$(show_split_services_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
}

View file

@ -0,0 +1,130 @@
#!/usr/bin/env bash
handle_split_services_stack_flow() {
local stack_dir="${1}"
local data_mode=""
local database_choice=""
local redis_choice=""
local proxy_mode=""
local summary_action=""
local stack_env_path=""
local stack_apps_path=""
local generated_compose_path=""
local save_selection_status=0
local render_compose_status=0
data_mode="$(show_split_services_data_mode_menu "${stack_dir}" || true)"
case "${data_mode}" in
"Back to topology selection" | "")
return "${FLOW_CONTINUE}"
;;
*)
if ! get_split_services_data_mode_id "${data_mode}" >/dev/null; then
show_warning_and_wait "Unknown data services mode: ${data_mode}"
return "${FLOW_CONTINUE}"
fi
;;
esac
database_choice="$(show_split_services_database_menu "${stack_dir}" || true)"
case "${database_choice}" in
"Back to topology selection" | "")
return "${FLOW_CONTINUE}"
;;
*)
if ! get_single_host_database_id "${database_choice}" >/dev/null; then
show_warning_and_wait "Unknown database choice: ${database_choice}"
return "${FLOW_CONTINUE}"
fi
;;
esac
redis_choice="$(show_split_services_redis_mode_menu "${stack_dir}" || true)"
case "${redis_choice}" in
"Back to topology selection" | "")
return "${FLOW_CONTINUE}"
;;
*)
if ! get_split_services_redis_id "${redis_choice}" >/dev/null; then
show_warning_and_wait "Unknown Redis choice: ${redis_choice}"
return "${FLOW_CONTINUE}"
fi
;;
esac
proxy_mode="$(show_split_services_proxy_mode_menu "${stack_dir}" || true)"
case "${proxy_mode}" in
"Back to topology selection" | "")
return "${FLOW_CONTINUE}"
;;
*)
if ! get_single_host_proxy_mode_id "${proxy_mode}" >/dev/null; then
show_warning_and_wait "Unknown reverse proxy mode: ${proxy_mode}"
return "${FLOW_CONTINUE}"
fi
;;
esac
summary_action="$(show_split_services_summary_menu "${stack_dir}" "${data_mode}" "${database_choice}" "${redis_choice}" "${proxy_mode}" || true)"
case "${summary_action}" in
"Yes, write stack files") ;;
"Back to topology selection" | "")
return "${FLOW_CONTINUE}"
;;
"Abort wizard to main menu")
handle_abort_wizard_flow "${stack_dir}"
return $?
;;
*)
show_warning_and_wait "Unknown split-services summary action: ${summary_action}"
return "${FLOW_CONTINUE}"
;;
esac
if save_split_services_selection "${stack_dir}" "${proxy_mode}" "${data_mode}" "${database_choice}" "${redis_choice}"; then
:
else
save_selection_status=$?
if [ "${save_selection_status}" -eq 2 ] || [ "${save_selection_status}" -eq 130 ]; then
return "${FLOW_CONTINUE}"
fi
case "${save_selection_status}" in
31)
show_warning_and_wait "Could not write the split-services env file for stack: ${stack_dir}" 3
;;
32)
show_warning_and_wait "Could not write the split-services wizard metadata in ${stack_dir}/metadata.json." 3
;;
33)
show_warning_and_wait "Split-services app selection is empty. Select at least one app before writing stack files." 3
;;
34)
show_warning_and_wait "Could not write the selected app metadata in ${stack_dir}/metadata.json." 3
;;
35)
show_warning_and_wait "Could not generate ${stack_dir}/apps.json from the selected split-services apps." 3
;;
*)
show_warning_and_wait "Could not save split-services selection for stack: ${stack_dir} (${save_selection_status})." 3
;;
esac
return "${FLOW_CONTINUE}"
fi
if render_stack_compose_from_metadata "${stack_dir}"; then
:
else
render_compose_status=$?
stack_env_path="$(get_stack_env_path "${stack_dir}")"
stack_apps_path="${stack_dir}/apps.json"
generated_compose_path="$(get_stack_generated_compose_path "${stack_dir}")"
show_warning_and_wait "Selection saved in ${stack_dir}/metadata.json, ${stack_env_path}, and ${stack_apps_path}, but compose rendering failed (${render_compose_status}) for ${generated_compose_path}." 3
return "${FLOW_CONTINUE}"
fi
stack_env_path="$(get_stack_env_path "${stack_dir}")"
stack_apps_path="${stack_dir}/apps.json"
generated_compose_path="$(get_stack_generated_compose_path "${stack_dir}")"
show_warning_and_wait "Split-services selection saved in ${stack_dir}/metadata.json, ${stack_env_path}, and ${stack_apps_path}. Rendered compose: ${generated_compose_path}." 3
return "${FLOW_OPEN_MANAGE_STACK}"
}

View file

@ -224,7 +224,7 @@ save_single_host_selection() {
fi
compose_files_lines="$(printf '%s\n%s' "${compose_files_lines}" "${proxy_overrides}")"
if collect_single_host_env_lines env_lines apps_metadata_json_object "${stack_dir}" "${proxy_mode_id}" "${database_id}"; then
if collect_single_host_env_lines env_lines apps_metadata_json_object "${stack_dir}" "${proxy_mode_id}" "${database_id}" "${redis_id}"; then
:
else
collect_env_status=$?

View file

@ -0,0 +1,164 @@
#!/usr/bin/env bash
get_split_services_data_mode_id() {
local data_mode="${1}"
case "${data_mode}" in
"Managed Data Services")
printf 'managed\n'
;;
"External Data Services")
printf 'external\n'
;;
*)
return 1
;;
esac
}
get_split_services_redis_id() {
local redis_choice="${1}"
case "${redis_choice}" in
"Managed Redis Services")
printf 'managed\n'
;;
"External Redis Services")
printf 'external\n'
;;
"No Redis Services")
printf 'disabled\n'
;;
*)
return 1
;;
esac
}
persist_split_services_selection_metadata() {
local stack_dir="${1}"
local proxy_mode_id="${2}"
local data_mode_id="${3}"
local database_id="${4}"
local redis_id="${5}"
local compose_files_lines="${6}"
local env_lines="${7}"
local updated_at=""
local compose_files_json=""
local env_json_object=""
local wizard_json_object=""
updated_at="$(get_current_utc_timestamp)"
compose_files_json="$(build_compose_files_json_array "${compose_files_lines}")"
env_json_object="$(build_env_json_object "${env_lines}")"
if ! wizard_json_object="$(
cat <<EOF
{
"topology": "split-services",
"selection": {
"proxy_mode_id": "${proxy_mode_id}",
"data_mode_id": "${data_mode_id}",
"database_id": "${database_id}",
"redis_id": "${redis_id}"
},
"env": ${env_json_object},
"compose_files": [
${compose_files_json}
],
"updated_at": "${updated_at}"
}
EOF
)"; then
return 1
fi
if ! persist_stack_metadata_wizard_object "${stack_dir}" "${wizard_json_object}"; then
return 1
fi
return 0
}
save_split_services_selection() {
local stack_dir="${1}"
local proxy_mode="${2}"
local data_mode="${3}"
local database_choice="${4}"
local redis_choice="${5}"
local proxy_mode_id=""
local data_mode_id=""
local database_id=""
local redis_id=""
local database_override=""
local redis_override=""
local proxy_overrides=""
local compose_files_lines=""
local env_lines=""
local apps_metadata_json_object=""
local collect_env_status=0
proxy_mode_id="$(get_single_host_proxy_mode_id "${proxy_mode}")" || return 1
data_mode_id="$(get_split_services_data_mode_id "${data_mode}")" || return 1
database_id="$(get_single_host_database_id "${database_choice}")" || return 1
redis_id="$(get_split_services_redis_id "${redis_choice}")" || return 1
if [ "${data_mode_id}" = "managed" ]; then
database_override="$(get_single_host_database_override "${database_choice}")" || return 1
fi
if [ "${redis_id}" = "managed" ]; then
redis_override="$(get_single_host_redis_override "Include Redis (recommended)")" || return 1
fi
proxy_overrides="$(get_single_host_proxy_overrides "${proxy_mode}")" || return 1
compose_files_lines="compose.yaml"
if [ -n "${database_override}" ]; then
compose_files_lines="$(printf '%s\n%s' "${compose_files_lines}" "${database_override}")"
fi
if [ -n "${redis_override}" ]; then
compose_files_lines="$(printf '%s\n%s' "${compose_files_lines}" "${redis_override}")"
fi
compose_files_lines="$(printf '%s\n%s' "${compose_files_lines}" "${proxy_overrides}")"
if collect_split_services_env_lines env_lines apps_metadata_json_object "${stack_dir}" "${proxy_mode_id}" "${data_mode_id}" "${database_id}" "${redis_id}"; then
:
else
collect_env_status=$?
return "${collect_env_status}"
fi
if ! persist_single_host_env_file "${stack_dir}" "${env_lines}"; then
return 31
fi
if persist_split_services_selection_metadata \
"${stack_dir}" \
"${proxy_mode_id}" \
"${data_mode_id}" \
"${database_id}" \
"${redis_id}" \
"${compose_files_lines}" \
"${env_lines}"; then
:
else
return 32
fi
if [ -z "${apps_metadata_json_object}" ]; then
return 33
fi
if persist_stack_metadata_apps_object "${stack_dir}" "${apps_metadata_json_object}"; then
:
else
return 34
fi
if ! persist_stack_apps_json_from_metadata_apps "${stack_dir}"; then
return 35
fi
return 0
}

View file

@ -8,6 +8,8 @@ load_production_screen_modules() {
source "${screen_dir}/production/setup.sh"
# shellcheck source=scripts/easy-docker/lib/ui/screens/production/topology.sh
source "${screen_dir}/production/topology.sh"
# shellcheck source=scripts/easy-docker/lib/ui/screens/production/split_services.sh
source "${screen_dir}/production/split_services.sh"
# shellcheck source=scripts/easy-docker/lib/ui/screens/production/manage.sh
source "${screen_dir}/production/manage.sh"
}

View file

@ -53,31 +53,44 @@ show_manage_stack_actions_menu() {
local stack_name="${1}"
local stack_dir="${2}"
local stack_runtime_status="${3:-Unknown}"
local stack_topology=""
local menu_header=""
local status_text=""
local -a menu_options=()
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}")"
stack_topology="$(get_stack_topology "${stack_dir}" || true)"
status_text="$(printf "Manage stack\n\nStack: %s\nDirectory: %s\nRuntime status: %s\nTopology: %s\n\nChoose an action for this stack." "${stack_name}" "${stack_dir}" "${stack_runtime_status}" "${stack_topology:-unknown}")"
render_box_message "${status_text}" "0 2" >&2
menu_header="$(printf "Stack actions | %s" "${stack_runtime_status}")"
menu_options=(
"Apps"
"Updates"
)
case "${stack_topology}" in
single-host)
menu_options+=("Site")
;;
esac
menu_options+=(
"Start stack in Docker Compose"
"Restart stack in Docker Compose"
"Stop stack in Docker Compose"
"Delete stack"
"Back"
"Exit and close easy-docker"
)
gum choose \
--height 12 \
--header "${menu_header}" \
--cursor.foreground 63 \
--selected.foreground 45 \
"Apps" \
"Updates" \
"Site" \
"Start stack in Docker Compose" \
"Restart stack in Docker Compose" \
"Stop stack in Docker Compose" \
"Delete stack" \
"Back" \
"Exit and close easy-docker"
"${menu_options[@]}"
}
show_manage_stack_apps_menu() {

View file

@ -0,0 +1,115 @@
#!/usr/bin/env bash
show_split_services_data_mode_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\nSplit-services setup (step 1/5)\nApplication Services run the Frappe image, workers, scheduler, and frontend.\nData Services provide the database and Redis layer.\n\nChoose how the data layer should be handled." "${stack_name}")"
render_box_message "${status_text}" "0 2" >&2
gum choose \
--height 9 \
--header "Data Services" \
--cursor.foreground 63 \
--selected.foreground 45 \
"Managed Data Services" \
"External Data Services" \
"Back to topology selection"
}
show_split_services_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\nSplit-services setup (step 2/5)\nChoose the database engine for the data layer.\nMariaDB is the default choice for most users.\nPostgreSQL is available if that is the database you want to run with this stack." "${stack_name}")"
render_box_message "${status_text}" "0 2" >&2
gum choose \
--height 8 \
--header "Data Services: database engine" \
--cursor.foreground 63 \
--selected.foreground 45 \
"MariaDB (recommended)" \
"PostgreSQL" \
"Back to topology selection"
}
show_split_services_redis_mode_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\nSplit-services setup (step 3/5)\nChoose how Redis should be handled.\nManaged Redis keeps the Redis services inside the generated stack.\nExternal Redis uses endpoints you provide manually.\nChoose no Redis services only if you know you want to handle Redis yourself." "${stack_name}")"
render_box_message "${status_text}" "0 2" >&2
gum choose \
--height 9 \
--header "Redis Services" \
--cursor.foreground 63 \
--selected.foreground 45 \
"Managed Redis Services" \
"External Redis Services" \
"No Redis Services" \
"Back to topology selection"
}
show_split_services_proxy_mode_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\nSplit-services setup (step 4/5)\nChoose the reverse proxy mode.\nThe reverse proxy is optional and can stay outside the stack if you already manage it elsewhere." "${stack_name}")"
render_box_message "${status_text}" "0 2" >&2
gum choose \
--height 11 \
--header "Reverse 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_split_services_summary_menu() {
local stack_dir="${1}"
local data_mode_label="${2}"
local database_label="${3}"
local redis_label="${4}"
local proxy_label="${5}"
local stack_name=""
local status_text=""
render_main_screen 1 >&2
stack_name="${stack_dir##*/}"
status_text="$(printf "Stack: %s\n\nSplit-services setup (step 5/5)\nReview the selected layout before the stack files are written.\n\nApplication Services: managed in this stack\nData Services: %s\nDatabase engine: %s\nRedis Services: %s\nReverse Proxy: %s" "${stack_name}" "${data_mode_label}" "${database_label}" "${redis_label}" "${proxy_label}")"
render_box_message "${status_text}" "0 2" >&2
gum choose \
--height 8 \
--header "Split-services summary" \
--cursor.foreground 63 \
--selected.foreground 45 \
"Yes, write stack files" \
"Back to topology selection" \
"Abort wizard to main menu"
}

View file

@ -8,7 +8,7 @@ show_stack_topology_menu() {
render_main_screen 1 >&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 (currently in development)." "${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 application services, data services, and an optional reverse proxy." "${stack_name}" "${stack_dir}")"
render_box_message "${status_text}" "0 2" >&2
gum choose \
@ -17,7 +17,7 @@ show_stack_topology_menu() {
--cursor.foreground 63 \
--selected.foreground 45 \
"Single-host (recommended)" \
"Split services (in development)" \
"Split services" \
"Abort wizard to main menu"
}
@ -29,12 +29,12 @@ show_single_host_proxy_menu() {
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}")"
status_text="$(printf "Stack: %s\n\nChoose the reverse 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" \
--header "Reverse proxy mode" \
--cursor.foreground 63 \
--selected.foreground 45 \
"Traefik (HTTP, built-in proxy)" \
@ -54,12 +54,12 @@ show_single_host_database_menu() {
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}")"
status_text="$(printf "Stack: %s\n\nChoose the database engine." "${stack_name}")"
render_box_message "${status_text}" "0 2" >&2
gum choose \
--height 8 \
--header "Single-host: database" \
--header "Database engine" \
--cursor.foreground 63 \
--selected.foreground 45 \
"MariaDB (recommended)" \
@ -75,12 +75,12 @@ show_single_host_redis_menu() {
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}")"
status_text="$(printf "Stack: %s\n\nChoose whether Redis services should be included in this stack." "${stack_name}")"
render_box_message "${status_text}" "0 2" >&2
gum choose \
--height 8 \
--header "Single-host: redis" \
--header "Redis services" \
--cursor.foreground 63 \
--selected.foreground 45 \
"Include Redis (recommended)" \
@ -169,23 +169,6 @@ prompt_single_host_env_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=""