diff --git a/CLAUDE.md b/CLAUDE.md index 93ea525..e0db519 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -125,9 +125,12 @@ log_error "message" # red [ERROR] ```bash prompt_yn "Question? (y/n):" "default_y_or_n" VARNAME prompt_text "Question? [default]:" "default" VARNAME +prompt_reinstall_mode VARNAME # sets VARNAME to: update | fresh | cancel ``` -When `UNATTENDED=true` both functions skip the prompt and use the default. +When `UNATTENDED=true` all three skip the prompt; `prompt_yn`/`prompt_text` use +their given default, `prompt_reinstall_mode` always resolves to `cancel`. See +**Update vs. fresh reinstall on rerun** below for how to use the latter. ### Pre-flight @@ -262,6 +265,51 @@ fi Put the check early — after any pure-display output (banners, info text) but before the first write. +## Update vs. fresh reinstall on rerun + +Every service should detect an existing install at the top of its +`install_()` — after the `DRY_RUN` check, before any prompts — and +offer `prompt_reinstall_mode` instead of silently re-running every prompt +(domain, secrets, firewall, Authelia, extras...) from scratch. What counts +as "already installed" is service-specific: usually `docker-compose.yml` and +`.env` both existing in the service's `$DOCKER_DIR/` directory. + +```bash +if [[ -f "$DIR/docker-compose.yml" && -f "$DIR/.env" ]]; then + local MODE="" + prompt_reinstall_mode MODE + case "$MODE" in + update) + # Refresh vendor files / config templates, rebuild, done. + # Do NOT touch .env, firewall rules, or Caddy/Authelia config. + ... + return 0 + ;; + cancel) + log_info "Leaving the existing install as-is." + return 0 + ;; + fresh) ;; # fall through to the full install flow below + esac +fi +``` + +`update` should be genuinely non-destructive: refresh whatever the service +vendors or templates (Docker image sources, config templates, +`docker-compose.yml`) and rebuild/restart, but never touch `.env`, firewall +rules, or reverse-proxy/SSO config that's already in place. If the +vendor-copy or `docker-compose.yml`-generation logic is more than a few +lines, factor it into a helper function so the fresh-install path and the +update path share one copy instead of drifting apart — see +`_asterisk_do_refresh_vendor_files`/`_asterisk_do_write_compose` in +`services/asterisk-do.sh` (and their `_asterisk_*` counterparts in +`services/asterisk.sh`) for the reference pattern. + +`cancel` must leave the install completely untouched — it's the default for +a reason (a stray Enter on a service you're just checking on shouldn't +trigger anything). `fresh` runs the exact same flow a first-time install +would, prompts included. + ## .env files and secrets Generate passwords with `generate_password` (never hardcode them). diff --git a/lib/common.sh b/lib/common.sh index 67265d7..91c00df 100644 --- a/lib/common.sh +++ b/lib/common.sh @@ -362,6 +362,32 @@ prompt_text() { eval "$varname='${response:-$default}'" } +# Prompt for how to handle a service that's already installed, honoring +# unattended. prompt_reinstall_mode VARNAME +# Sets VARNAME to one of: update | fresh | cancel +# Enter (no input) and any unrecognized input both resolve to "cancel" — this +# guards a destructive full reinstall behind a deliberate keypress instead of +# a stray Enter. Unattended mode always resolves to "cancel" too: never +# silently touch an existing install when nobody's watching the prompt. +prompt_reinstall_mode() { + local varname="$1" response + if [ "$UNATTENDED" = true ]; then + eval "$varname='cancel'" + echo "Existing install detected — leaving it as-is [auto: cancel, unattended mode]" + return + fi + echo " Existing install detected. Choose:" + echo " r) Reinstall in place — refresh vendor files/config, keep existing settings" + echo " f) Full install — re-run every prompt from scratch" + echo " c) Cancel — leave everything as-is [default]" + read -p " Choice [r/f/c, Enter=cancel]: " response + case "${response,,}" in + r) eval "$varname='update'" ;; + f) eval "$varname='fresh'" ;; + *) eval "$varname='cancel'" ;; + esac +} + # ── Per-service README generation ──────────────────────────────────────────── # Write /README.md from stdin (markdown). Every module is encouraged to # call this so each ~/docker// folder is self-documenting. diff --git a/services/asterisk-do.sh b/services/asterisk-do.sh index b5b8029..9379bbf 100755 --- a/services/asterisk-do.sh +++ b/services/asterisk-do.sh @@ -61,6 +61,25 @@ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then eval "$_var='${_r:-$_def}'" } + prompt_reinstall_mode() { + local _var="$1" _r + if [[ "${UNATTENDED:-false}" == "true" ]]; then + eval "$_var='cancel'" + echo "Existing install detected — leaving it as-is [auto: cancel, unattended mode]" + return + fi + echo " Existing install detected. Choose:" + echo " r) Reinstall in place — refresh vendor files/config, keep existing settings" + echo " f) Full install — re-run every prompt from scratch" + echo " c) Cancel — leave everything as-is [default]" + read -r -p " Choice [r/f/c, Enter=cancel]: " _r + case "${_r,,}" in + r) eval "$_var='update'" ;; + f) eval "$_var='fresh'" ;; + *) eval "$_var='cancel'" ;; + esac + } + configure_caddy_for_service() { local _name="$1" _upstream="$2" _subdomain="$3" _extra="${4:-}" local _caddy_dir="$DOCKER_DIR/caddy" @@ -353,39 +372,47 @@ install_asterisk-do() { if [[ -f "$EA_DIR/docker-compose.yml" && -f "$EA_DIR/.env" ]]; then echo "" log_info "Existing install found at $EA_DIR." - local UPDATE_INPLACE="" - prompt_yn "Update in place (refresh vendor files + docker-compose.yml, keep your existing domain/firewall/Authelia settings) instead of a full fresh reinstall? (y/n):" "y" UPDATE_INPLACE - if [[ "$UPDATE_INPLACE" =~ ^[Yy]$ ]]; then - mkdir -p "$EA_DIR/config/asterisk" "$EA_DIR/config/easy-asterisk" \ - "$EA_DIR/logs" "$EA_DIR/spool" "$EA_DIR/lib" "$EA_DIR/exports" - ensure_docker_dir_ownership "$EA_DIR" - cd "$EA_DIR" || return 1 + local REINSTALL_MODE="" + prompt_reinstall_mode REINSTALL_MODE + case "$REINSTALL_MODE" in + update) + mkdir -p "$EA_DIR/config/asterisk" "$EA_DIR/config/easy-asterisk" \ + "$EA_DIR/logs" "$EA_DIR/spool" "$EA_DIR/lib" "$EA_DIR/exports" + ensure_docker_dir_ownership "$EA_DIR" + cd "$EA_DIR" || return 1 - _asterisk_do_refresh_vendor_files - _asterisk_do_write_compose + _asterisk_do_refresh_vendor_files + _asterisk_do_write_compose - log_info "Rebuilding and restarting containers..." - if docker compose up -d --build --force-recreate; then - log_success "Update complete — vendor files and docker-compose.yml refreshed." - else - log_warning "docker compose up failed — check: docker compose -f $EA_DIR/docker-compose.yml logs" - fi + log_info "Rebuilding and restarting containers..." + if docker compose up -d --build --force-recreate; then + log_success "Update complete — vendor files and docker-compose.yml refreshed." + else + log_warning "docker compose up failed — check: docker compose -f $EA_DIR/docker-compose.yml logs" + fi - local _EXISTING_DOMAIN _EXISTING_PORT - _EXISTING_DOMAIN="$(grep -E '^DOMAIN_NAME=' .env | cut -d= -f2-)" - _EXISTING_PORT="$(grep -E '^WEB_ADMIN_PORT=' .env | cut -d= -f2-)" - echo "" - log_success "Existing .env, UFW rules, Cloud Firewall, and Caddy/Authelia config were left untouched." - if [[ -n "$_EXISTING_DOMAIN" ]]; then - echo " Web admin: https://${_EXISTING_DOMAIN}/" - else - echo " Web admin: http://:${_EXISTING_PORT:-8081}" - fi - echo " Logs: docker compose -f $EA_DIR/docker-compose.yml logs -f" - echo "" - return 0 - fi - log_info "Proceeding with a full fresh reinstall — existing config will be reused where prompts match, everything else re-asked." + local _EXISTING_DOMAIN _EXISTING_PORT + _EXISTING_DOMAIN="$(grep -E '^DOMAIN_NAME=' .env | cut -d= -f2-)" + _EXISTING_PORT="$(grep -E '^WEB_ADMIN_PORT=' .env | cut -d= -f2-)" + echo "" + log_success "Existing .env, UFW rules, Cloud Firewall, and Caddy/Authelia config were left untouched." + if [[ -n "$_EXISTING_DOMAIN" ]]; then + echo " Web admin: https://${_EXISTING_DOMAIN}/" + else + echo " Web admin: http://:${_EXISTING_PORT:-8081}" + fi + echo " Logs: docker compose -f $EA_DIR/docker-compose.yml logs -f" + echo "" + return 0 + ;; + cancel) + log_info "Leaving the existing install as-is — nothing changed." + return 0 + ;; + fresh) + log_info "Proceeding with a full fresh reinstall — every prompt below runs from scratch." + ;; + esac fi # ── Bring in base first, if this is a genuinely fresh box ───────────────── diff --git a/services/asterisk.sh b/services/asterisk.sh index 66da5b7..418bd66 100644 --- a/services/asterisk.sh +++ b/services/asterisk.sh @@ -59,6 +59,25 @@ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then eval "$_var='${_r:-$_def}'" } + prompt_reinstall_mode() { + local _var="$1" _r + if [[ "${UNATTENDED:-false}" == "true" ]]; then + eval "$_var='cancel'" + echo "Existing install detected — leaving it as-is [auto: cancel, unattended mode]" + return + fi + echo " Existing install detected. Choose:" + echo " r) Reinstall in place — refresh vendor files/config, keep existing settings" + echo " f) Full install — re-run every prompt from scratch" + echo " c) Cancel — leave everything as-is [default]" + read -r -p " Choice [r/f/c, Enter=cancel]: " _r + case "${_r,,}" in + r) eval "$_var='update'" ;; + f) eval "$_var='fresh'" ;; + *) eval "$_var='cancel'" ;; + esac + } + configure_caddy_for_service() { local _name="$1" _upstream="$2" _subdomain="$3" _extra="${4:-}" local _caddy_dir="$DOCKER_DIR/caddy" @@ -333,39 +352,47 @@ install_asterisk() { if [[ -f "$EA_DIR/docker-compose.yml" && -f "$EA_DIR/.env" ]]; then echo "" log_info "Existing install found at $EA_DIR." - local UPDATE_INPLACE="" - prompt_yn "Update in place (refresh vendor files + docker-compose.yml, keep your existing domain/VLAN/Authelia settings) instead of a full fresh reinstall? (y/n):" "y" UPDATE_INPLACE - if [[ "$UPDATE_INPLACE" =~ ^[Yy]$ ]]; then - mkdir -p "$EA_DIR/config/asterisk" "$EA_DIR/config/easy-asterisk" \ - "$EA_DIR/logs" "$EA_DIR/spool" "$EA_DIR/lib" "$EA_DIR/exports" - ensure_docker_dir_ownership "$EA_DIR" - cd "$EA_DIR" || return 1 + local REINSTALL_MODE="" + prompt_reinstall_mode REINSTALL_MODE + case "$REINSTALL_MODE" in + update) + mkdir -p "$EA_DIR/config/asterisk" "$EA_DIR/config/easy-asterisk" \ + "$EA_DIR/logs" "$EA_DIR/spool" "$EA_DIR/lib" "$EA_DIR/exports" + ensure_docker_dir_ownership "$EA_DIR" + cd "$EA_DIR" || return 1 - _asterisk_refresh_vendor_files - _asterisk_write_compose + _asterisk_refresh_vendor_files + _asterisk_write_compose - log_info "Rebuilding and restarting containers..." - if docker compose up -d --build --force-recreate; then - log_success "Update complete — vendor files and docker-compose.yml refreshed." - else - log_warning "docker compose up failed — check: docker compose -f $EA_DIR/docker-compose.yml logs" - fi + log_info "Rebuilding and restarting containers..." + if docker compose up -d --build --force-recreate; then + log_success "Update complete — vendor files and docker-compose.yml refreshed." + else + log_warning "docker compose up failed — check: docker compose -f $EA_DIR/docker-compose.yml logs" + fi - local _EXISTING_DOMAIN _EXISTING_PORT - _EXISTING_DOMAIN="$(grep -E '^DOMAIN_NAME=' .env | cut -d= -f2-)" - _EXISTING_PORT="$(grep -E '^WEB_ADMIN_PORT=' .env | cut -d= -f2-)" - echo "" - log_success "Existing .env, UFW rules, and Caddy/Authelia config were left untouched." - if [[ -n "$_EXISTING_DOMAIN" ]]; then - echo " Web admin: https://${_EXISTING_DOMAIN}/" - else - echo " Web admin: http://$(hostname -I 2>/dev/null | awk '{print $1}' || echo localhost):${_EXISTING_PORT:-8081}" - fi - echo " Logs: docker compose -f $EA_DIR/docker-compose.yml logs -f" - echo "" - return 0 - fi - log_info "Proceeding with a full fresh reinstall — existing config will be reused where prompts match, everything else re-asked." + local _EXISTING_DOMAIN _EXISTING_PORT + _EXISTING_DOMAIN="$(grep -E '^DOMAIN_NAME=' .env | cut -d= -f2-)" + _EXISTING_PORT="$(grep -E '^WEB_ADMIN_PORT=' .env | cut -d= -f2-)" + echo "" + log_success "Existing .env, UFW rules, and Caddy/Authelia config were left untouched." + if [[ -n "$_EXISTING_DOMAIN" ]]; then + echo " Web admin: https://${_EXISTING_DOMAIN}/" + else + echo " Web admin: http://$(hostname -I 2>/dev/null | awk '{print $1}' || echo localhost):${_EXISTING_PORT:-8081}" + fi + echo " Logs: docker compose -f $EA_DIR/docker-compose.yml logs -f" + echo "" + return 0 + ;; + cancel) + log_info "Leaving the existing install as-is — nothing changed." + return 0 + ;; + fresh) + log_info "Proceeding with a full fresh reinstall — every prompt below runs from scratch." + ;; + esac fi mkdir -p "$EA_DIR"