From 3c3e92a815fb95ed46f94e995c5965ea92a5356c Mon Sep 17 00:00:00 2001 From: RocketQuack <202538874+Rocket-Quack@users.noreply.github.com> Date: Wed, 25 Mar 2026 14:01:48 +0100 Subject: [PATCH] feat(easy-docker): improve stack runtime controls and isolation --- .../lib/app/wizard/common/compose/render.sh | 6 +- .../lib/app/wizard/common/compose/start.sh | 249 ++++++++++++++++-- .../easy-docker/lib/app/wizard/common/core.sh | 24 ++ .../lib/app/wizard/flows/manage.sh | 35 +++ .../lib/ui/screens/production/manage.sh | 3 +- 5 files changed, 297 insertions(+), 20 deletions(-) diff --git a/scripts/easy-docker/lib/app/wizard/common/compose/render.sh b/scripts/easy-docker/lib/app/wizard/common/compose/render.sh index b6691146..b55ca4e1 100755 --- a/scripts/easy-docker/lib/app/wizard/common/compose/render.sh +++ b/scripts/easy-docker/lib/app/wizard/common/compose/render.sh @@ -11,11 +11,13 @@ render_stack_compose_from_metadata() { local source_compose_path="" local env_erpnext_version="" local fallback_erpnext_version="" + local compose_project_name="" local repo_root="" local -a compose_args=() metadata_path="${stack_dir}/metadata.json" env_path="$(get_stack_env_path "${stack_dir}")" + compose_project_name="$(get_stack_compose_project_name "${stack_dir}")" generated_compose_path="$(get_stack_generated_compose_path "${stack_dir}")" generated_compose_tmp_path="${generated_compose_path}.tmp" @@ -58,11 +60,11 @@ EOF fi if [ -n "${fallback_erpnext_version}" ]; then - if ! ERPNEXT_VERSION="${fallback_erpnext_version}" docker compose --env-file "${env_path}" "${compose_args[@]}" config >"${generated_compose_tmp_path}"; then + if ! ERPNEXT_VERSION="${fallback_erpnext_version}" docker compose --project-name "${compose_project_name}" --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 - elif ! docker compose --env-file "${env_path}" "${compose_args[@]}" config >"${generated_compose_tmp_path}"; then + elif ! docker compose --project-name "${compose_project_name}" --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 diff --git a/scripts/easy-docker/lib/app/wizard/common/compose/start.sh b/scripts/easy-docker/lib/app/wizard/common/compose/start.sh index 2cfde7e7..be75672c 100755 --- a/scripts/easy-docker/lib/app/wizard/common/compose/start.sh +++ b/scripts/easy-docker/lib/app/wizard/common/compose/start.sh @@ -15,6 +15,7 @@ start_stack_with_compose_from_metadata() { local custom_tag="" local image_ref="" local image_inspect_error="" + local compose_project_name="" local stack_topology="" local repo_root="" local -a compose_args=() @@ -24,6 +25,7 @@ start_stack_with_compose_from_metadata() { metadata_path="${stack_dir}/metadata.json" env_path="$(get_stack_env_path "${stack_dir}")" + compose_project_name="$(get_stack_compose_project_name "${stack_dir}")" if [ ! -f "${metadata_path}" ]; then return 31 @@ -110,24 +112,112 @@ EOF fi if [ -n "${fallback_erpnext_version}" ] && [ -n "${runtime_pull_policy}" ]; then - if ! ERPNEXT_VERSION="${fallback_erpnext_version}" PULL_POLICY="${runtime_pull_policy}" docker compose --env-file "${env_path}" "${compose_args[@]}" up -d; then + if ! ERPNEXT_VERSION="${fallback_erpnext_version}" PULL_POLICY="${runtime_pull_policy}" docker compose --project-name "${compose_project_name}" --env-file "${env_path}" "${compose_args[@]}" up -d; then return 37 fi elif [ -n "${fallback_erpnext_version}" ]; then - if ! ERPNEXT_VERSION="${fallback_erpnext_version}" docker compose --env-file "${env_path}" "${compose_args[@]}" up -d; then + if ! ERPNEXT_VERSION="${fallback_erpnext_version}" docker compose --project-name "${compose_project_name}" --env-file "${env_path}" "${compose_args[@]}" up -d; then return 37 fi elif [ -n "${runtime_pull_policy}" ]; then - if ! PULL_POLICY="${runtime_pull_policy}" docker compose --env-file "${env_path}" "${compose_args[@]}" up -d; then + if ! PULL_POLICY="${runtime_pull_policy}" docker compose --project-name "${compose_project_name}" --env-file "${env_path}" "${compose_args[@]}" up -d; then return 37 fi - elif ! docker compose --env-file "${env_path}" "${compose_args[@]}" up -d; then + elif ! docker compose --project-name "${compose_project_name}" --env-file "${env_path}" "${compose_args[@]}" up -d; then return 37 fi return 0 } +stop_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 compose_project_name="" + local stack_topology="" + local repo_root="" + local -a compose_args=() + + # shellcheck disable=SC2034 # Read by manage flow after stop_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}")" + compose_project_name="$(get_stack_compose_project_name "${stack_dir}")" + + if [ ! -f "${metadata_path}" ]; then + return 41 + fi + + if [ ! -f "${env_path}" ]; then + return 42 + fi + + stack_topology="$(get_stack_topology "${stack_dir}" || true)" + if [ -z "${stack_topology}" ]; then + # shellcheck disable=SC2034 # Read by manage flow after stop_stack_with_compose_from_metadata returns 43. + EASY_DOCKER_COMPOSE_ERROR_DETAIL="metadata.json missing topology" + return 43 + fi + + case "${stack_topology}" in + "single-host") ;; + *) + # shellcheck disable=SC2034 # Read by manage flow after stop_stack_with_compose_from_metadata returns 44. + EASY_DOCKER_COMPOSE_ERROR_DETAIL="${stack_topology}" + return 44 + ;; + 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 + + compose_files_lines="$(get_metadata_compose_files_lines "${metadata_path}" || true)" + if [ -z "${compose_files_lines}" ]; then + return 45 + 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 stop_stack_with_compose_from_metadata returns 46. + EASY_DOCKER_COMPOSE_ERROR_DETAIL="${source_compose_path}" + return 46 + fi + + compose_args+=(-f "${source_compose_path}") + done </dev/null + container_ids_lines="$( + ERPNEXT_VERSION="${fallback_erpnext_version}" docker compose --project-name "${compose_project_name}" --env-file "${env_path}" "${compose_args[@]}" ps -a -q 2>/dev/null )" compose_status=$? else - running_services_lines="$( - docker compose --env-file "${env_path}" "${compose_args[@]}" ps --status running --services 2>/dev/null + container_ids_lines="$( + docker compose --project-name "${compose_project_name}" --env-file "${env_path}" "${compose_args[@]}" ps -a -q 2>/dev/null )" compose_status=$? fi @@ -223,18 +331,125 @@ EOF return 0 fi - while IFS= read -r compose_file; do - if [ -n "${compose_file}" ]; then - running_services_count=$((running_services_count + 1)) + if [ -z "${container_ids_lines}" ]; then + printf -v "${result_var}" "%s" "Not created" + return 0 + fi + + docker_ps_args=(-a --no-trunc --format "{{.ID}}|{{.State}}|{{.Status}}") + while IFS= read -r container_id; do + if [ -n "${container_id}" ]; then + docker_ps_args+=(--filter "id=${container_id}") fi done </dev/null)" + compose_status=$? + if [ "${compose_status}" -ne 0 ]; then + printf -v "${result_var}" "%s" "Unknown (docker ps status failed)" + return 0 + fi + + while IFS= read -r container_status_line; do + if [ -z "${container_status_line}" ]; then + continue + fi + + total_containers_count=$((total_containers_count + 1)) + IFS='|' read -r container_id container_state container_status_text < ${EASY_DOCKER_COMPOSE_ERROR_DETAIL}" 4 + ;; + 47) + show_warning_and_wait "docker compose stop failed. Check the output above for details." 4 + ;; + *) + show_warning_and_wait "Cannot stop stack with docker compose (${compose_start_status})." 4 + ;; + esac + ;; "Docker") while true; do docker_action="$(show_manage_stack_docker_menu "${stack_name}" "${stack_dir}" || true)" diff --git a/scripts/easy-docker/lib/ui/screens/production/manage.sh b/scripts/easy-docker/lib/ui/screens/production/manage.sh index d61ecc58..2a74884f 100755 --- a/scripts/easy-docker/lib/ui/screens/production/manage.sh +++ b/scripts/easy-docker/lib/ui/screens/production/manage.sh @@ -65,13 +65,14 @@ show_manage_stack_actions_menu() { menu_header="$(printf "Stack actions | %s" "${stack_runtime_status}")" gum choose \ - --height 8 \ + --height 9 \ --header "${menu_header}" \ --cursor.foreground 63 \ --selected.foreground 45 \ "Apps" \ "Docker" \ "Start stack in Docker Compose" \ + "Stop stack in Docker Compose" \ "Back" \ "Exit and close easy-docker" }