fix(easy-docker): preserve custom apps and simplify manage flows

This commit is contained in:
RocketQuack 2026-04-20 14:30:09 +02:00
parent c754d05544
commit 6fdd9a3b84
6 changed files with 95 additions and 317 deletions

View file

@ -253,126 +253,3 @@ persist_stack_apps_json_from_metadata_apps() {
return 0
}
persist_stack_metadata_top_level_object() {
local stack_dir="${1}"
local object_key="${2}"
local object_json="${3}"
local insert_before_key="${4:-}"
local metadata_path=""
local metadata_tmp_path=""
metadata_path="${stack_dir}/metadata.json"
metadata_tmp_path="${metadata_path}.tmp"
if [ ! -f "${metadata_path}" ]; then
return 1
fi
if [ -z "${object_json}" ]; then
return 1
fi
if ! awk -v object_key="${object_key}" -v object_json="${object_json}" -v insert_before_key="${insert_before_key}" '
BEGIN {
target_regex = "^ \"" object_key "\"[[:space:]]*:"
before_regex = ""
if (insert_before_key != "") {
before_regex = "^ \"" insert_before_key "\"[[:space:]]*:"
}
in_target = 0
target_depth = 0
inserted = 0
prev = ""
}
function flush_prev() {
if (prev != "") {
print prev
prev = ""
}
}
{
if (!in_target && $0 ~ target_regex) {
flush_prev()
if (object_key == "wizard") {
print " \"" object_key "\": " object_json
} else {
print " \"" object_key "\": " object_json ","
}
in_target = 1
inserted = 1
if ($0 ~ /{/) {
target_depth += gsub(/{/, "{", $0)
target_depth -= gsub(/}/, "}", $0)
} else {
target_depth = 0
}
if (target_depth <= 0) {
in_target = 0
}
next
}
if (in_target) {
target_depth += gsub(/{/, "{", $0)
target_depth -= gsub(/}/, "}", $0)
if (target_depth <= 0) {
in_target = 0
}
next
}
if (!inserted && before_regex != "" && $0 ~ before_regex) {
flush_prev()
print " \"" object_key "\": " object_json ","
inserted = 1
}
if (!inserted && $0 ~ /^}/) {
if (prev != "") {
if (prev !~ /,[[:space:]]*$/) {
prev = prev ","
}
print prev
prev = ""
}
print " \"" object_key "\": " object_json
inserted = 1
print $0
next
}
flush_prev()
prev = $0
}
END {
flush_prev()
if (!inserted) {
exit 2
}
}
' "${metadata_path}" >"${metadata_tmp_path}"; then
rm -f -- "${metadata_tmp_path}" >/dev/null 2>&1 || true
return 1
fi
if ! mv -- "${metadata_tmp_path}" "${metadata_path}"; then
rm -f -- "${metadata_tmp_path}" >/dev/null 2>&1 || true
return 1
fi
return 0
}
persist_stack_metadata_apps_object() {
local stack_dir="${1}"
local apps_json_object="${2}"
persist_stack_metadata_top_level_object "${stack_dir}" "apps" "${apps_json_object}" "wizard"
}
persist_stack_metadata_wizard_object() {
local stack_dir="${1}"
local wizard_json_object="${2}"
persist_stack_metadata_top_level_object "${stack_dir}" "wizard" "${wizard_json_object}"
}

View file

@ -1,8 +1,10 @@
#!/usr/bin/env bash
persist_stack_metadata_apps_object() {
persist_stack_metadata_top_level_object() {
local stack_dir="${1}"
local apps_json_object="${2}"
local object_key="${2}"
local object_json="${3}"
local insert_before_key="${4:-}"
local metadata_path=""
local metadata_tmp_path=""
@ -12,14 +14,19 @@ persist_stack_metadata_apps_object() {
return 1
fi
if [ -z "${apps_json_object}" ]; then
if [ -z "${object_json}" ]; then
return 1
fi
if ! awk -v apps_object="${apps_json_object}" '
if ! awk -v object_key="${object_key}" -v object_json="${object_json}" -v insert_before_key="${insert_before_key}" '
BEGIN {
in_top_level_apps = 0
apps_depth = 0
target_regex = "^ \"" object_key "\"[[:space:]]*:"
before_regex = ""
if (insert_before_key != "") {
before_regex = "^ \"" insert_before_key "\"[[:space:]]*:"
}
in_target = 0
target_depth = 0
inserted = 0
prev = ""
}
@ -30,35 +37,39 @@ persist_stack_metadata_apps_object() {
}
}
{
if (!in_top_level_apps && $0 ~ /^ "apps"[[:space:]]*:/) {
if (!in_target && $0 ~ target_regex) {
flush_prev()
print " \"apps\": " apps_object ","
in_top_level_apps = 1
if (object_key == "wizard") {
print " \"" object_key "\": " object_json
} else {
print " \"" object_key "\": " object_json ","
}
in_target = 1
inserted = 1
if ($0 ~ /{/) {
apps_depth += gsub(/{/, "{", $0)
apps_depth -= gsub(/}/, "}", $0)
target_depth += gsub(/{/, "{", $0)
target_depth -= gsub(/}/, "}", $0)
} else {
apps_depth = 0
target_depth = 0
}
if (apps_depth <= 0) {
in_top_level_apps = 0
if (target_depth <= 0) {
in_target = 0
}
next
}
if (in_top_level_apps) {
apps_depth += gsub(/{/, "{", $0)
apps_depth -= gsub(/}/, "}", $0)
if (apps_depth <= 0) {
in_top_level_apps = 0
if (in_target) {
target_depth += gsub(/{/, "{", $0)
target_depth -= gsub(/}/, "}", $0)
if (target_depth <= 0) {
in_target = 0
}
next
}
if (!inserted && $0 ~ /^ "wizard"[[:space:]]*:/) {
if (!inserted && before_regex != "" && $0 ~ before_regex) {
flush_prev()
print " \"apps\": " apps_object ","
print " \"" object_key "\": " object_json ","
inserted = 1
}
@ -70,7 +81,7 @@ persist_stack_metadata_apps_object() {
print prev
prev = ""
}
print " \"apps\": " apps_object
print " \"" object_key "\": " object_json
inserted = 1
print $0
next
@ -98,94 +109,16 @@ persist_stack_metadata_apps_object() {
return 0
}
persist_stack_metadata_apps_object() {
local stack_dir="${1}"
local apps_json_object="${2}"
persist_stack_metadata_top_level_object "${stack_dir}" "apps" "${apps_json_object}" "wizard"
}
persist_stack_metadata_wizard_object() {
local stack_dir="${1}"
local wizard_json_object="${2}"
local metadata_path=""
local metadata_tmp_path=""
metadata_path="${stack_dir}/metadata.json"
metadata_tmp_path="${metadata_path}.tmp"
if [ ! -f "${metadata_path}" ]; then
return 1
fi
if [ -z "${wizard_json_object}" ]; then
return 1
fi
if ! awk -v wizard_object="${wizard_json_object}" '
BEGIN {
in_top_level_wizard = 0
wizard_depth = 0
inserted = 0
prev = ""
}
function flush_prev() {
if (prev != "") {
print prev
prev = ""
}
}
{
if (!in_top_level_wizard && $0 ~ /^ "wizard"[[:space:]]*:/) {
flush_prev()
print " \"wizard\": " wizard_object
in_top_level_wizard = 1
inserted = 1
if ($0 ~ /{/) {
wizard_depth += gsub(/{/, "{", $0)
wizard_depth -= gsub(/}/, "}", $0)
} else {
wizard_depth = 0
}
if (wizard_depth <= 0) {
in_top_level_wizard = 0
}
next
}
if (in_top_level_wizard) {
wizard_depth += gsub(/{/, "{", $0)
wizard_depth -= gsub(/}/, "}", $0)
if (wizard_depth <= 0) {
in_top_level_wizard = 0
}
next
}
if (!inserted && $0 ~ /^}/) {
if (prev != "") {
if (prev !~ /,[[:space:]]*$/) {
prev = prev ","
}
print prev
prev = ""
}
print " \"wizard\": " wizard_object
inserted = 1
print $0
next
}
flush_prev()
prev = $0
}
END {
flush_prev()
if (!inserted) {
exit 2
}
}
' "${metadata_path}" >"${metadata_tmp_path}"; then
rm -f -- "${metadata_tmp_path}" >/dev/null 2>&1 || true
return 1
fi
if ! mv -- "${metadata_tmp_path}" "${metadata_path}"; then
rm -f -- "${metadata_tmp_path}" >/dev/null 2>&1 || true
return 1
fi
return 0
persist_stack_metadata_top_level_object "${stack_dir}" "wizard" "${wizard_json_object}"
}

View file

@ -84,12 +84,17 @@ build_predefined_apps_metadata_json_object() {
local result_var="${1}"
local predefined_csv="${2}"
local branch_lines="${3}"
local custom_apps_lines="${4:-}"
local app_id=""
local app_branch=""
local custom_repo=""
local custom_branch=""
local predefined_json_entries=""
local branch_json_entries=""
local custom_json_entries=""
local escaped_app_id=""
local escaped_branch=""
local escaped_repo=""
local entry_json=""
local line=""
local -a predefined_ids=()
@ -134,7 +139,30 @@ build_predefined_apps_metadata_json_object() {
${branch_lines}
EOF
printf -v "${result_var}" '{\n "predefined": [\n%s\n ],\n "predefined_branches": {\n%s\n },\n "custom": [\n ]\n }' "${predefined_json_entries}" "${branch_json_entries}"
while IFS= read -r line; do
if [ -z "${line}" ]; then
continue
fi
custom_repo="${line%%|*}"
custom_branch="${line#*|}"
if [ -z "${custom_repo}" ] || [ -z "${custom_branch}" ]; then
continue
fi
escaped_repo="$(json_escape_string "${custom_repo}")"
escaped_branch="$(json_escape_string "${custom_branch}")"
entry_json="$(printf ' {\n "repo": "%s",\n "branch": "%s"\n }' "${escaped_repo}" "${escaped_branch}")"
if [ -z "${custom_json_entries}" ]; then
custom_json_entries="${entry_json}"
else
custom_json_entries="${custom_json_entries}"$',\n'"${entry_json}"
fi
done <<EOF
${custom_apps_lines}
EOF
printf -v "${result_var}" '{\n "predefined": [\n%s\n ],\n "predefined_branches": {\n%s\n },\n "custom": [\n%s\n ]\n }' "${predefined_json_entries}" "${branch_json_entries}" "${custom_json_entries}"
}
get_predefined_branch_from_lines() {
@ -247,6 +275,7 @@ prompt_custom_modular_apps_data() {
local preferred_branch=""
local available_branch_lines=""
local existing_branch_lines=""
local existing_custom_lines=""
local selected_branch_lines=""
local selected_app_count=0
local assembled_apps_metadata_json_object=""
@ -257,6 +286,7 @@ prompt_custom_modular_apps_data() {
if [ -f "${metadata_path}" ]; then
selected_predefined_csv="$(get_metadata_apps_predefined_csv "${metadata_path}" || true)"
existing_branch_lines="$(get_metadata_apps_predefined_branch_lines "${metadata_path}" || true)"
existing_custom_lines="$(get_metadata_apps_custom_lines "${metadata_path}" || true)"
fi
while true; do
@ -393,7 +423,7 @@ EOF
continue
fi
build_predefined_apps_metadata_json_object assembled_apps_metadata_json_object "${selected_predefined_csv}" "${selected_branch_lines}"
build_predefined_apps_metadata_json_object assembled_apps_metadata_json_object "${selected_predefined_csv}" "${selected_branch_lines}" "${existing_custom_lines}"
printf -v "${result_apps_metadata_var}" "%s" "${assembled_apps_metadata_json_object}"
return 0
done
@ -444,6 +474,7 @@ prompt_selected_stack_app_branches_data() {
local preferred_branch=""
local available_branch_lines=""
local existing_branch_lines=""
local existing_custom_lines=""
local selected_branch_lines=""
local selected_app_count=0
local assembled_apps_metadata_json_object=""
@ -457,6 +488,7 @@ prompt_selected_stack_app_branches_data() {
selected_predefined_csv="$(get_metadata_apps_predefined_csv "${metadata_path}" || true)"
existing_branch_lines="$(get_metadata_apps_predefined_branch_lines "${metadata_path}" || true)"
existing_custom_lines="$(get_metadata_apps_custom_lines "${metadata_path}" || true)"
if [ -z "${selected_predefined_csv}" ]; then
return 4
fi
@ -515,7 +547,7 @@ prompt_selected_stack_app_branches_data() {
return 4
fi
build_predefined_apps_metadata_json_object assembled_apps_metadata_json_object "${selected_predefined_csv}" "${selected_branch_lines}"
build_predefined_apps_metadata_json_object assembled_apps_metadata_json_object "${selected_predefined_csv}" "${selected_branch_lines}" "${existing_custom_lines}"
printf -v "${result_apps_metadata_var}" "%s" "${assembled_apps_metadata_json_object}"
return 0
}

View file

@ -4,8 +4,8 @@ load_easy_docker_manage_flow_modules() {
local manage_dir=""
manage_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/manage"
# shellcheck source=scripts/easy-docker/lib/app/wizard/flows/manage/docker.sh
source "${manage_dir}/docker.sh"
# shellcheck source=scripts/easy-docker/lib/app/wizard/flows/manage/build.sh
source "${manage_dir}/build.sh"
# shellcheck source=scripts/easy-docker/lib/app/wizard/flows/manage/prompts.sh
source "${manage_dir}/prompts.sh"
# shellcheck source=scripts/easy-docker/lib/app/wizard/flows/manage/site.sh

View file

@ -1,5 +1,21 @@
#!/usr/bin/env bash
refresh_stack_generated_compose_with_feedback() {
local stack_dir="${1}"
local generated_compose_path=""
local render_compose_status=0
generated_compose_path="$(get_stack_generated_compose_path "${stack_dir}")"
if render_stack_compose_from_metadata "${stack_dir}"; then
show_warning_and_wait "Generated compose refreshed successfully: ${generated_compose_path}" 3
return 0
fi
render_compose_status=$?
show_warning_and_wait "The image build succeeded, but generated compose could not be refreshed (${render_compose_status}) for ${generated_compose_path}." 4
return "${render_compose_status}"
}
run_build_stack_custom_image_with_feedback() {
local stack_name="${1}"
local stack_dir="${2}"
@ -8,6 +24,7 @@ run_build_stack_custom_image_with_feedback() {
show_warning_message "Starting docker build for stack: ${stack_name}"
if build_stack_custom_image "${stack_dir}"; then
show_warning_and_wait "Custom image build finished successfully for stack: ${stack_name}" 3
refresh_stack_generated_compose_with_feedback "${stack_dir}" || true
return 0
else
build_image_status=$?

View file

@ -1,81 +0,0 @@
#!/usr/bin/env bash
refresh_stack_generated_compose_with_feedback() {
local stack_dir="${1}"
local generated_compose_path=""
local render_compose_status=0
generated_compose_path="$(get_stack_generated_compose_path "${stack_dir}")"
if render_stack_compose_from_metadata "${stack_dir}"; then
show_warning_and_wait "Generated compose refreshed successfully: ${generated_compose_path}" 3
return 0
fi
render_compose_status=$?
show_warning_and_wait "The image build succeeded, but generated compose could not be refreshed (${render_compose_status}) for ${generated_compose_path}." 4
return "${render_compose_status}"
}
run_build_stack_custom_image_with_feedback() {
local stack_name="${1}"
local stack_dir="${2}"
local build_image_status=0
show_warning_message "Starting docker build for stack: ${stack_name}"
if build_stack_custom_image "${stack_dir}"; then
show_warning_and_wait "Custom image build finished successfully for stack: ${stack_name}" 3
refresh_stack_generated_compose_with_feedback "${stack_dir}" || true
return 0
else
build_image_status=$?
fi
case "${build_image_status}" in
11)
show_warning_and_wait "Custom image build failed: missing metadata.json in ${stack_dir}." 4
;;
12)
show_warning_and_wait "Custom image build failed: stack env file not found in ${stack_dir}." 4
;;
13)
show_warning_and_wait "Custom image build failed: CUSTOM_IMAGE is missing in stack env file." 4
;;
14)
show_warning_and_wait "Custom image build failed: CUSTOM_TAG is missing in stack env file." 4
;;
15)
show_warning_and_wait "Custom image build failed: frappe_branch missing in metadata.json." 4
;;
16)
show_warning_and_wait "Custom image build failed: could not generate apps.json from metadata app selection." 4
;;
17)
show_warning_and_wait "Custom image build failed: apps.json not found after generation." 4
;;
18)
show_warning_and_wait "Custom image build failed: base64 command is not available in this environment." 4
;;
19)
show_warning_and_wait "Custom image build failed: apps.json could not be base64-encoded." 4
;;
20)
show_warning_and_wait "Custom image build failed: images/layered/Containerfile not found." 4
;;
21)
show_warning_and_wait "Custom image build failed: docker build returned an error. Check the output above." 4
;;
22)
show_warning_and_wait "Custom image build failed: git is required for app branch precheck (git ls-remote)." 4
;;
23)
show_warning_and_wait "Custom image build failed: could not parse app entries from apps.json." 4
;;
24)
show_warning_and_wait "Custom image build failed: app branch precheck failed -> ${EASY_DOCKER_BUILD_ERROR_DETAIL}" 6
;;
*)
show_warning_and_wait "Custom image build failed (${build_image_status})." 4
;;
esac
return "${build_image_status}"
}