mirror of
https://github.com/frappe/frappe_docker.git
synced 2026-06-17 13:55:08 +00:00
feat(easy-docker): add guarded stack and site deletion
This commit is contained in:
parent
7c4d1d47ca
commit
1a839299ab
5 changed files with 364 additions and 1 deletions
|
|
@ -218,6 +218,115 @@ EOF
|
|||
return 0
|
||||
}
|
||||
|
||||
delete_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 custom_image=""
|
||||
local custom_tag=""
|
||||
local custom_image_ref=""
|
||||
local -a compose_args=()
|
||||
|
||||
# shellcheck disable=SC2034 # Read by manage flow after delete_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 48
|
||||
fi
|
||||
|
||||
if [ ! -f "${env_path}" ]; then
|
||||
return 49
|
||||
fi
|
||||
|
||||
stack_topology="$(get_stack_topology "${stack_dir}" || true)"
|
||||
if [ -z "${stack_topology}" ]; then
|
||||
EASY_DOCKER_COMPOSE_ERROR_DETAIL="metadata.json missing topology"
|
||||
return 50
|
||||
fi
|
||||
|
||||
case "${stack_topology}" in
|
||||
"single-host") ;;
|
||||
*)
|
||||
EASY_DOCKER_COMPOSE_ERROR_DETAIL="${stack_topology}"
|
||||
return 51
|
||||
;;
|
||||
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
|
||||
|
||||
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
|
||||
custom_image_ref="${custom_image}:${custom_tag}"
|
||||
fi
|
||||
|
||||
compose_files_lines="$(get_metadata_compose_files_lines "${metadata_path}" || true)"
|
||||
if [ -z "${compose_files_lines}" ]; then
|
||||
return 52
|
||||
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
|
||||
EASY_DOCKER_COMPOSE_ERROR_DETAIL="${source_compose_path}"
|
||||
return 53
|
||||
fi
|
||||
|
||||
compose_args+=(-f "${source_compose_path}")
|
||||
done <<EOF
|
||||
${compose_files_lines}
|
||||
EOF
|
||||
|
||||
if [ "${#compose_args[@]}" -eq 0 ]; then
|
||||
return 52
|
||||
fi
|
||||
|
||||
if [ -n "${fallback_erpnext_version}" ]; then
|
||||
if ! ERPNEXT_VERSION="${fallback_erpnext_version}" docker compose --project-name "${compose_project_name}" --env-file "${env_path}" "${compose_args[@]}" down -v --remove-orphans --rmi local; then
|
||||
return 54
|
||||
fi
|
||||
elif ! docker compose --project-name "${compose_project_name}" --env-file "${env_path}" "${compose_args[@]}" down -v --remove-orphans --rmi local; then
|
||||
return 54
|
||||
fi
|
||||
|
||||
if [ -n "${custom_image_ref}" ]; then
|
||||
if docker image inspect "${custom_image_ref}" >/dev/null 2>&1; then
|
||||
if ! docker image rm "${custom_image_ref}"; then
|
||||
EASY_DOCKER_COMPOSE_ERROR_DETAIL="${custom_image_ref}"
|
||||
return 55
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if ! rollback_stack_directory "${stack_dir}"; then
|
||||
# shellcheck disable=SC2034 # Read by manage flow after delete_stack_with_compose_from_metadata returns 56.
|
||||
EASY_DOCKER_COMPOSE_ERROR_DETAIL="${stack_dir}"
|
||||
return 56
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
get_stack_compose_runtime_status_label() {
|
||||
local result_var="${1}"
|
||||
local stack_dir="${2}"
|
||||
|
|
|
|||
|
|
@ -741,6 +741,45 @@ cleanup_partial_stack_site() {
|
|||
return 0
|
||||
}
|
||||
|
||||
delete_configured_stack_site() {
|
||||
local stack_dir="${1}"
|
||||
local site_name=""
|
||||
local delete_status=0
|
||||
|
||||
if ! stack_supports_single_site_management "${stack_dir}"; then
|
||||
return 52
|
||||
fi
|
||||
|
||||
site_name="$(get_stack_site_name "${stack_dir}" || true)"
|
||||
if ! is_safe_stack_site_cleanup_name "${site_name}"; then
|
||||
return 61
|
||||
fi
|
||||
|
||||
if ! stack_backend_service_is_running "${stack_dir}"; then
|
||||
return 51
|
||||
fi
|
||||
|
||||
if cleanup_partial_stack_site "${stack_dir}" "${site_name}"; then
|
||||
:
|
||||
else
|
||||
delete_status=$?
|
||||
case "${delete_status}" in
|
||||
54 | 52 | 61)
|
||||
return "${delete_status}"
|
||||
;;
|
||||
*)
|
||||
return 60
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
if ! clear_stack_site_metadata "${stack_dir}"; then
|
||||
return 58
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
create_first_stack_site() {
|
||||
local stack_dir="${1}"
|
||||
local site_name="${2}"
|
||||
|
|
|
|||
|
|
@ -352,3 +352,11 @@ mark_stack_site_failed() {
|
|||
updated_at="$(get_current_utc_timestamp)"
|
||||
persist_stack_site_metadata "${stack_dir}" "single-site" "${site_name}" "failed" "${apps_installed_lines}" "${last_action}" "${last_error}" "${error_log_path}" "${created_at}" "${updated_at}"
|
||||
}
|
||||
|
||||
clear_stack_site_metadata() {
|
||||
local stack_dir="${1}"
|
||||
local updated_at=""
|
||||
|
||||
updated_at="$(get_current_utc_timestamp)"
|
||||
persist_stack_site_metadata "${stack_dir}" "single-site" "" "not_created" "" "delete-site" "" "" "" "${updated_at}"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -128,6 +128,35 @@ prompt_manage_stack_site_admin_password_with_cancel() {
|
|||
done
|
||||
}
|
||||
|
||||
prompt_manage_stack_delete_keyword_with_cancel() {
|
||||
local result_var="${1}"
|
||||
local stack_name="${2}"
|
||||
local delete_confirmation=""
|
||||
local prompt_status=0
|
||||
|
||||
while true; do
|
||||
delete_confirmation="$(prompt_manage_stack_delete_keyword "${stack_name}")"
|
||||
prompt_status=$?
|
||||
if [ "${prompt_status}" -ne 0 ]; then
|
||||
return "${FLOW_ABORT_INPUT}"
|
||||
fi
|
||||
|
||||
delete_confirmation="$(printf '%s' "${delete_confirmation}" | tr -d '\r\n')"
|
||||
case "${delete_confirmation}" in
|
||||
/back | /Back | /BACK | "")
|
||||
return "${FLOW_ABORT_INPUT}"
|
||||
;;
|
||||
delete)
|
||||
printf -v "${result_var}" "%s" "${delete_confirmation}"
|
||||
return "${FLOW_CONTINUE}"
|
||||
;;
|
||||
*)
|
||||
show_warning_and_wait "Type exactly delete to confirm stack removal." 3
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
handle_manage_stack_site_flow() {
|
||||
local stack_name="${1}"
|
||||
local stack_dir="${2}"
|
||||
|
|
@ -141,6 +170,7 @@ handle_manage_stack_site_flow() {
|
|||
local existing_site_apps_lines=""
|
||||
local existing_site_apps_csv=""
|
||||
local existing_site_details_action=""
|
||||
local site_delete_confirmation=""
|
||||
|
||||
while true; do
|
||||
existing_site_entry=""
|
||||
|
|
@ -239,6 +269,59 @@ handle_manage_stack_site_flow() {
|
|||
"${existing_site_apps_csv}" || true
|
||||
)"
|
||||
case "${existing_site_details_action}" in
|
||||
"Delete site")
|
||||
site_delete_confirmation="$(
|
||||
show_manage_stack_site_delete_confirmation \
|
||||
"${stack_name}" \
|
||||
"${stack_dir}" \
|
||||
"${existing_site_name}" || true
|
||||
)"
|
||||
case "${site_delete_confirmation}" in
|
||||
"Yes")
|
||||
show_warning_message "Deleting site for stack: ${stack_name}"
|
||||
if delete_configured_stack_site "${stack_dir}"; then
|
||||
show_warning_and_wait "Site deleted successfully with its database: ${existing_site_name}" 3
|
||||
continue
|
||||
fi
|
||||
|
||||
site_flow_status=$?
|
||||
case "${site_flow_status}" in
|
||||
51)
|
||||
show_warning_and_wait "Cannot delete site: backend service is not running yet. Start the stack first." 4
|
||||
;;
|
||||
52)
|
||||
show_warning_and_wait "Cannot delete site for this topology yet. Only single-host stacks are supported." 4
|
||||
;;
|
||||
54)
|
||||
show_warning_and_wait "Cannot delete site because stack metadata, env, or compose inputs are incomplete." 4
|
||||
;;
|
||||
58)
|
||||
show_warning_and_wait "The cleared site state could not be written to metadata.json." 4
|
||||
;;
|
||||
60)
|
||||
show_warning_and_wait "Site deletion could not remove all site or database data automatically. Manual cleanup is required." 5
|
||||
;;
|
||||
61)
|
||||
show_warning_and_wait "Cannot delete site because the configured site name was empty or unsafe for cleanup operations." 4
|
||||
;;
|
||||
*)
|
||||
show_warning_and_wait "Site deletion failed (${site_flow_status})." 4
|
||||
;;
|
||||
esac
|
||||
continue
|
||||
;;
|
||||
"No" | "")
|
||||
continue
|
||||
;;
|
||||
"Exit and close easy-docker")
|
||||
return "${FLOW_EXIT_APP}"
|
||||
;;
|
||||
*)
|
||||
show_warning_and_wait "Unknown site delete confirmation action: ${site_delete_confirmation}" 2
|
||||
continue
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
"Back" | "")
|
||||
continue
|
||||
;;
|
||||
|
|
@ -273,6 +356,8 @@ handle_manage_selected_stack_flow() {
|
|||
local generated_compose_path=""
|
||||
local stack_runtime_status=""
|
||||
local missing_custom_image_action=""
|
||||
local delete_stack_confirmation_action=""
|
||||
local delete_stack_keyword=""
|
||||
|
||||
stack_dir="$(get_stack_dir_by_name "${stack_name}" || true)"
|
||||
if [ -z "${stack_dir}" ]; then
|
||||
|
|
@ -449,6 +534,70 @@ handle_manage_selected_stack_flow() {
|
|||
;;
|
||||
esac
|
||||
;;
|
||||
"Delete stack")
|
||||
delete_stack_confirmation_action="$(
|
||||
show_manage_stack_delete_confirmation "${stack_name}" "${stack_dir}" || true
|
||||
)"
|
||||
case "${delete_stack_confirmation_action}" in
|
||||
"Yes")
|
||||
if ! prompt_manage_stack_delete_keyword_with_cancel delete_stack_keyword "${stack_name}"; then
|
||||
continue
|
||||
fi
|
||||
if [ "${delete_stack_keyword}" != "delete" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
show_warning_message "Deleting stack with docker compose resources: ${stack_name}"
|
||||
if delete_stack_with_compose_from_metadata "${stack_dir}"; then
|
||||
show_warning_and_wait "Stack deleted successfully with containers, networks, volumes, image, and stack directory: ${stack_name}" 5
|
||||
return "${FLOW_CONTINUE}"
|
||||
fi
|
||||
|
||||
compose_start_status=$?
|
||||
case "${compose_start_status}" in
|
||||
48)
|
||||
show_warning_and_wait "Cannot delete stack: metadata.json is missing in ${stack_dir}." 4
|
||||
;;
|
||||
49)
|
||||
show_warning_and_wait "Cannot delete stack: stack env file not found in ${stack_dir}." 4
|
||||
;;
|
||||
50)
|
||||
show_warning_and_wait "Cannot delete stack: topology is missing in metadata.json. Re-run the topology wizard for this stack." 4
|
||||
;;
|
||||
51)
|
||||
show_warning_and_wait "Cannot delete stack via docker compose for topology '${EASY_DOCKER_COMPOSE_ERROR_DETAIL}'. Use the topology-specific runbook path." 5
|
||||
;;
|
||||
52)
|
||||
show_warning_and_wait "Cannot delete stack: no compose files configured in metadata.json." 4
|
||||
;;
|
||||
53)
|
||||
show_warning_and_wait "Cannot delete stack: compose file is missing -> ${EASY_DOCKER_COMPOSE_ERROR_DETAIL}" 4
|
||||
;;
|
||||
54)
|
||||
show_warning_and_wait "docker compose down failed. Check the output above for details." 4
|
||||
;;
|
||||
55)
|
||||
show_warning_and_wait "Stack resources were removed, but the configured custom image could not be deleted -> ${EASY_DOCKER_COMPOSE_ERROR_DETAIL}" 5
|
||||
;;
|
||||
56)
|
||||
show_warning_and_wait "Docker resources were removed, but the stack directory could not be deleted -> ${EASY_DOCKER_COMPOSE_ERROR_DETAIL}" 5
|
||||
;;
|
||||
*)
|
||||
show_warning_and_wait "Cannot delete stack with docker compose (${compose_start_status})." 4
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
"No" | "")
|
||||
continue
|
||||
;;
|
||||
"Exit and close easy-docker")
|
||||
return "${FLOW_EXIT_APP}"
|
||||
;;
|
||||
*)
|
||||
show_warning_and_wait "Unknown delete-stack action: ${delete_stack_confirmation_action}" 2
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
"Docker")
|
||||
while true; do
|
||||
docker_action="$(show_manage_stack_docker_menu "${stack_name}" "${stack_dir}" || true)"
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ show_manage_stack_actions_menu() {
|
|||
menu_header="$(printf "Stack actions | %s" "${stack_runtime_status}")"
|
||||
|
||||
gum choose \
|
||||
--height 10 \
|
||||
--height 11 \
|
||||
--header "${menu_header}" \
|
||||
--cursor.foreground 63 \
|
||||
--selected.foreground 45 \
|
||||
|
|
@ -74,6 +74,7 @@ show_manage_stack_actions_menu() {
|
|||
"Site" \
|
||||
"Start stack in Docker Compose" \
|
||||
"Stop stack in Docker Compose" \
|
||||
"Delete stack" \
|
||||
"Back" \
|
||||
"Exit and close easy-docker"
|
||||
}
|
||||
|
|
@ -205,10 +206,67 @@ show_manage_stack_site_details() {
|
|||
--header "Site details" \
|
||||
--cursor.foreground 63 \
|
||||
--selected.foreground 45 \
|
||||
"Delete site" \
|
||||
"Back" \
|
||||
"Exit and close easy-docker"
|
||||
}
|
||||
|
||||
show_manage_stack_site_delete_confirmation() {
|
||||
local stack_name="${1}"
|
||||
local stack_dir="${2}"
|
||||
local site_name="${3}"
|
||||
local status_text=""
|
||||
|
||||
render_main_screen 1 >&2
|
||||
|
||||
status_text="$(printf "Delete site\n\nStack: %s\nDirectory: %s\nSite: %s\n\nAll site data and the site database will be permanently deleted." "${stack_name}" "${stack_dir}" "${site_name}")"
|
||||
render_box_message "${status_text}" "0 2" >&2
|
||||
|
||||
gum choose \
|
||||
--height 8 \
|
||||
--header "Confirm delete site" \
|
||||
--cursor.foreground 63 \
|
||||
--selected.foreground 45 \
|
||||
"Yes" \
|
||||
"No" \
|
||||
"Exit and close easy-docker"
|
||||
}
|
||||
|
||||
show_manage_stack_delete_confirmation() {
|
||||
local stack_name="${1}"
|
||||
local stack_dir="${2}"
|
||||
local status_text=""
|
||||
|
||||
render_main_screen 1 >&2
|
||||
|
||||
status_text="$(printf "Delete stack\n\nStack: %s\nDirectory: %s\n\nThis will permanently remove the stack directory, Docker containers, networks, volumes, and configured custom image." "${stack_name}" "${stack_dir}")"
|
||||
render_box_message "${status_text}" "0 2" >&2
|
||||
|
||||
gum choose \
|
||||
--height 8 \
|
||||
--header "Confirm delete stack" \
|
||||
--cursor.foreground 63 \
|
||||
--selected.foreground 45 \
|
||||
"Yes" \
|
||||
"No" \
|
||||
"Exit and close easy-docker"
|
||||
}
|
||||
|
||||
prompt_manage_stack_delete_keyword() {
|
||||
local stack_name="${1}"
|
||||
local status_text=""
|
||||
|
||||
render_main_screen 1 >&2
|
||||
|
||||
status_text="$(printf "Delete stack\n\nStack: %s\n\nFinal confirmation required.\nType delete to permanently remove the stack and all its data.\nType /back or press Ctrl+C to cancel." "${stack_name}")"
|
||||
render_box_message "${status_text}" "0 2" >&2
|
||||
|
||||
gum input \
|
||||
--header "Type delete to confirm" \
|
||||
--prompt "confirm> " \
|
||||
--placeholder "delete"
|
||||
}
|
||||
|
||||
show_missing_custom_image_start_menu() {
|
||||
local stack_name="${1}"
|
||||
local stack_dir="${2}"
|
||||
|
|
|
|||
Loading…
Reference in a new issue