diff --git a/install-update-oidc-mobile-bridge.sh b/install-update-oidc-mobile-bridge.sh index d8994b7..343c3a0 100755 --- a/install-update-oidc-mobile-bridge.sh +++ b/install-update-oidc-mobile-bridge.sh @@ -35,6 +35,7 @@ SYMLINK_CHANGED=false UNIT_CREATED=false ROLLBACK_TARGET="" LEGACY_BRIDGE_BACKUP="" +REINSTALL_BRIDGE_BACKUP="" # ---------- @@ -162,6 +163,7 @@ ask_yes_no() { rollback_bridge() { local rollback_failed=false local rollback_path="" + local rollback_tmp_link="${MM_BRIDGE_LINK}.rollback.$$" [[ "$SYMLINK_CHANGED" = true ]] || return 0 @@ -189,10 +191,11 @@ rollback_bridge() { if [[ "$rollback_failed" = true ]]; then error "Initial-install cleanup was not fully successful. Please check '${MM_BRIDGE_LINK}', '${MM_BRIDGE_SERVICE}' and nginx manually." + return 1 else info "Initial activation was removed. The new binary, source directory and unit file were kept for analysis." + return 0 fi - return 0 fi warn "The new bridge did not pass all checks. Trying to restore the previous installation." @@ -205,25 +208,85 @@ rollback_bridge() { fi fi - if [[ -n "$ROLLBACK_TARGET" && -e "$rollback_path" ]]; then - ln -s "$ROLLBACK_TARGET" "${MM_BRIDGE_LINK}.rollback.$$" >> "$log_file" 2>&1 \ - && mv -Tf "${MM_BRIDGE_LINK}.rollback.$$" "$MM_BRIDGE_LINK" >> "$log_file" 2>&1 \ - || rollback_failed=true - elif [[ -n "$LEGACY_BRIDGE_BACKUP" && -f "$LEGACY_BRIDGE_BACKUP" ]]; then - rm -f "$MM_BRIDGE_LINK" >> "$log_file" 2>&1 - cp -a "$LEGACY_BRIDGE_BACKUP" "$MM_BRIDGE_LINK" >> "$log_file" 2>&1 || rollback_failed=true - else - rollback_failed=true + # A same-version reinstall replaces the file referenced by ROLLBACK_TARGET. + # Restore that saved binary first; changing the symlink alone would otherwise + # still point to the newly installed (and possibly broken) binary. + if [[ -n "$REINSTALL_BRIDGE_BACKUP" ]]; then + echononl "Restore previous same-version bridge binary.." + _restore_tmp="${MM_BRIDGE_BIN}.restore.$$" + rm -f "$_restore_tmp" + + if [[ -f "$REINSTALL_BRIDGE_BACKUP" ]] \ + && cp -a "$REINSTALL_BRIDGE_BACKUP" "$_restore_tmp" >> "$log_file" 2>&1 \ + && mv -Tf "$_restore_tmp" "$MM_BRIDGE_BIN" >> "$log_file" 2>&1 ; then + echo_ok + else + rm -f "$_restore_tmp" + echo_failed + error "Could not restore '${REINSTALL_BRIDGE_BACKUP}' to '${MM_BRIDGE_BIN}'." + rollback_failed=true + fi fi if [[ "$rollback_failed" = false ]]; then - systemctl restart "$MM_BRIDGE_SERVICE" >> "$log_file" 2>&1 || rollback_failed=true + if [[ -n "$ROLLBACK_TARGET" && -e "$rollback_path" ]]; then + echononl "Restore previous stable bridge symlink.." + rm -f "$rollback_tmp_link" + if ln -s "$ROLLBACK_TARGET" "$rollback_tmp_link" >> "$log_file" 2>&1 \ + && mv -Tf "$rollback_tmp_link" "$MM_BRIDGE_LINK" >> "$log_file" 2>&1 ; then + echo_ok + else + rm -f "$rollback_tmp_link" + echo_failed + error "Could not restore stable bridge symlink '${MM_BRIDGE_LINK}' -> '${ROLLBACK_TARGET}'." + rollback_failed=true + fi + elif [[ -n "$LEGACY_BRIDGE_BACKUP" && -f "$LEGACY_BRIDGE_BACKUP" ]]; then + echononl "Restore previous legacy bridge binary.." + if rm -f "$MM_BRIDGE_LINK" >> "$log_file" 2>&1 \ + && cp -a "$LEGACY_BRIDGE_BACKUP" "$MM_BRIDGE_LINK" >> "$log_file" 2>&1 ; then + echo_ok + else + echo_failed + error "Could not restore legacy bridge binary from '${LEGACY_BRIDGE_BACKUP}'." + rollback_failed=true + fi + else + error "No usable rollback target was found." + rollback_failed=true + fi + fi + + if [[ "$rollback_failed" = false ]]; then + echononl "Restart previous Mattermost OIDC Mobile Bridge service.." + if systemctl restart "$MM_BRIDGE_SERVICE" >> "$log_file" 2>&1 ; then + echo_ok + else + echo_failed + error "Could not restart '${MM_BRIDGE_SERVICE}' after rollback." + rollback_failed=true + fi + fi + + if [[ "$NGINX_CHANGED" = true && -n "$NGINX_BACKUP" && -f "$NGINX_BACKUP" ]]; then + echononl "Restore previous nginx configuration.." + if cp -a "$NGINX_BACKUP" "$NGINX_CONFIG_FILE" >> "$log_file" 2>&1 \ + && nginx -t >> "$log_file" 2>&1 \ + && systemctl reload nginx.service >> "$log_file" 2>&1 ; then + echo_ok + else + echo_failed + error "Could not fully restore/reload the previous nginx configuration. Check nginx manually." + rollback_failed=true + fi fi if [[ "$rollback_failed" = true ]]; then - error "Automatic rollback failed. Please check '${MM_BRIDGE_LINK}' and '${MM_BRIDGE_SERVICE}' manually." + error "Automatic rollback failed. Please check the bridge installation, '${MM_BRIDGE_SERVICE}' and nginx configuration manually." + return 1 else info "Previous bridge installation was restored and restarted." + return 0 fi } @@ -777,7 +840,14 @@ blank_line if [[ -e "$MM_BRIDGE_BIN" ]]; then echononl "Backup existing bridge binary.." - if mv "$MM_BRIDGE_BIN" "${MM_BRIDGE_BIN}.${backup_date}" > "$log_file" 2>&1 ; then + _bridge_binary_backup="${MM_BRIDGE_BIN}.${backup_date}" + if mv "$MM_BRIDGE_BIN" "$_bridge_binary_backup" > "$log_file" 2>&1 ; then + # During a same-version reinstall this backup is the actual rollback + # payload. ROLLBACK_TARGET alone is insufficient because it names the + # same versioned path that is about to be replaced. + if [[ "$INSTALLATION_MODE" = "upgrade" && "$MM_NEW_VERSION" = "$MM_CURRENT_VERSION" ]]; then + REINSTALL_BRIDGE_BACKUP="$_bridge_binary_backup" + fi echo_ok else echo_failed @@ -797,6 +867,25 @@ else fatal "$(cat "$log_file")" fi +# On an initial installation the systemd unit below already references the +# stable bridge symlink. Create that symlink before systemd-analyze verifies +# the unit. During upgrades, keep the existing symlink untouched until the +# final activation step. +if [[ "$INSTALLATION_MODE" = "initial-installation" ]]; then + echononl "Create initial stable bridge symlink.." + _tmp_link="${MM_BRIDGE_LINK}.new.$$" + rm -f "$_tmp_link" + if ln -s "${MM_BRIDGE_NAME}-${MM_NEW_VERSION}" "$_tmp_link" > "$log_file" 2>&1 \ + && mv -Tf "$_tmp_link" "$MM_BRIDGE_LINK" >> "$log_file" 2>&1 ; then + SYMLINK_CHANGED=true + echo_ok + else + rm -f "$_tmp_link" + echo_failed + fatal "$(cat "$log_file")" + fi +fi + # Preserve an old unversioned installation before replacing it with a symlink. if [[ "$INSTALLATION_MODE" = "legacy-upgrade" ]]; then LEGACY_BRIDGE_BACKUP="${MM_BRIDGE_LINK}.${backup_date}" @@ -898,6 +987,20 @@ else fi +# Ensure that the bridge service is enabled also when an existing unit is reused. +echononl "Ensure '${MM_BRIDGE_SERVICE}' is enabled.." +if systemctl is-enabled --quiet "$MM_BRIDGE_SERVICE" 2>/dev/null ; then + echo_ok +else + if systemctl enable "$MM_BRIDGE_SERVICE" > "$log_file" 2>&1 ; then + echo_ok + else + echo_failed + fatal "$(cat "$log_file")" + fi +fi + + # ---------- # Configure nginx if requested # ---------- @@ -991,7 +1094,7 @@ NGINX_MOBILE_EOF fi echononl "Insert missing bridge locations before Mattermost 'location /'.." - python3 - "$NGINX_CONFIG_FILE" "$NGINX_INSERT_FILE" "$MM_BRIDGE_UPSTREAM" <<'PY_EOF' > "$log_file" 2>&1 + python3 - "$NGINX_CONFIG_FILE" "$NGINX_INSERT_FILE" "$MM_BRIDGE_UPSTREAM" "$_mm_site_host" <<'PY_EOF' > "$log_file" 2>&1 import re import sys from pathlib import Path @@ -999,6 +1102,7 @@ from pathlib import Path config = Path(sys.argv[1]) insert = Path(sys.argv[2]).read_text() upstream = sys.argv[3].rstrip("/") +site_host = sys.argv[4].strip() text = config.read_text() def matching_brace(data, open_pos): @@ -1032,25 +1136,53 @@ def matching_brace(data, open_pos): return pos return None -servers = [] +all_servers = [] for match in re.finditer(r'(?m)^[ \t]*server[ \t]*\{', text): open_pos = text.find("{", match.start(), match.end()) close_pos = matching_brace(text, open_pos) if close_pos is None: raise SystemExit("Could not parse nginx server block") body = text[open_pos + 1:close_pos] + all_servers.append((open_pos + 1, close_pos, body)) + +def general_locations(body): + return list(re.finditer(r'(?m)^[ \t]*location[ \t]+/[ \t]*\{', body)) + +# Prefer the Mattermost vHost identified by SiteURL/server_name. This works +# with both direct proxy_pass targets and named nginx upstreams. The HTTP +# redirect vHost is excluded because it has no general "location /" block. +servers = [] +if site_host: + for server in all_servers: + body = server[2] + names = [] + for match in re.finditer(r'(?m)^[ \t]*server_name[ \t]+([^;]+);', body): + names.extend(match.group(1).split()) + if site_host in names and len(general_locations(body)) == 1: + servers.append(server) + +# Fallback for installations where SiteURL is unavailable: retain the old +# direct-upstream detection. +if not servers: proxy_re = re.compile(r'proxy_pass\s+' + re.escape(upstream) + r'/?\s*;') - if proxy_re.search(body): - servers.append((open_pos + 1, close_pos, body)) + servers = [ + server for server in all_servers + if proxy_re.search(server[2]) and len(general_locations(server[2])) == 1 + ] if len(servers) != 1: - raise SystemExit(f"Expected exactly one server block proxying to {upstream}, found {len(servers)}") + if site_host: + raise SystemExit( + f"Expected exactly one Mattermost server block for server_name " + f"{site_host} with a general 'location /', found {len(servers)}" + ) + raise SystemExit( + f"Expected exactly one server block proxying to {upstream} " + f"with a general 'location /', found {len(servers)}" + ) body_start, body_end, body = servers[0] -locations = list(re.finditer(r'(?m)^[ \t]*location[ \t]+/[ \t]*\{', body)) -if len(locations) != 1: - raise SystemExit(f"Expected exactly one general 'location /' in Mattermost server block, found {len(locations)}") - +locations = general_locations(body) insert_pos = body_start + locations[0].start() text = text[:insert_pos] + insert + text[insert_pos:] config.write_text(text) @@ -1089,17 +1221,24 @@ blank_line echo -e "\033[37m\033[1mActivate Mattermost OIDC Mobile Bridge..\033[m" blank_line -echononl "Switch stable bridge symlink to '${MM_BRIDGE_NAME}-${MM_NEW_VERSION}'.." -_tmp_link="${MM_BRIDGE_LINK}.new.$$" -rm -f "$_tmp_link" -if ln -s "${MM_BRIDGE_NAME}-${MM_NEW_VERSION}" "$_tmp_link" > "$log_file" 2>&1 \ - && mv -Tf "$_tmp_link" "$MM_BRIDGE_LINK" >> "$log_file" 2>&1 ; then - SYMLINK_CHANGED=true +if [[ "$INSTALLATION_MODE" = "initial-installation" ]] \ + && [[ -L "$MM_BRIDGE_LINK" ]] \ + && [[ "$(readlink "$MM_BRIDGE_LINK")" = "${MM_BRIDGE_NAME}-${MM_NEW_VERSION}" ]]; then + echononl "Keep initial stable bridge symlink to '${MM_BRIDGE_NAME}-${MM_NEW_VERSION}'.." echo_ok else + echononl "Switch stable bridge symlink to '${MM_BRIDGE_NAME}-${MM_NEW_VERSION}'.." + _tmp_link="${MM_BRIDGE_LINK}.new.$$" rm -f "$_tmp_link" - echo_failed - fatal "$(cat "$log_file")" + if ln -s "${MM_BRIDGE_NAME}-${MM_NEW_VERSION}" "$_tmp_link" > "$log_file" 2>&1 \ + && mv -Tf "$_tmp_link" "$MM_BRIDGE_LINK" >> "$log_file" 2>&1 ; then + SYMLINK_CHANGED=true + echo_ok + else + rm -f "$_tmp_link" + echo_failed + fatal "$(cat "$log_file")" + fi fi if [[ "$NGINX_CHANGED" = true ]]; then @@ -1109,14 +1248,32 @@ if [[ "$NGINX_CHANGED" = true ]]; then else echo_failed error "nginx reload failed. Restoring the previous nginx configuration." + + nginx_rollback_failed=false + cp -a "$NGINX_CONFIG_FILE" "${NGINX_CONFIG_FILE}.failed-${backup_date}" 2>/dev/null || true - if cp -a "$NGINX_BACKUP" "$NGINX_CONFIG_FILE" >> "$log_file" 2>&1 && nginx -t >> "$log_file" 2>&1 && systemctl reload nginx.service >> "$log_file" 2>&1 ; then + + if cp -a "$NGINX_BACKUP" "$NGINX_CONFIG_FILE" >> "$log_file" 2>&1 \ + && nginx -t >> "$log_file" 2>&1 \ + && systemctl reload nginx.service >> "$log_file" 2>&1 ; then info "Previous nginx configuration was restored and reloaded." + NGINX_CHANGED=false else error "Could not fully restore/reload the previous nginx configuration. Check nginx manually." + nginx_rollback_failed=true + fi + + if rollback_bridge ; then + bridge_rollback_failed=false + else + bridge_rollback_failed=true + fi + + if [[ "$nginx_rollback_failed" = false && "$bridge_rollback_failed" = false ]]; then + fatal "Installation of Mattermost OIDC Mobile Bridge ${MM_NEW_VERSION} failed because nginx could not be reloaded.\n The previous nginx configuration and bridge installation were successfully restored." + else + fatal "Installation of Mattermost OIDC Mobile Bridge ${MM_NEW_VERSION} failed because nginx could not be reloaded.\n The previous system state could not be fully restored.\n\n Manual intervention is required." fi - rollback_bridge - fatal "nginx reload failed after adding the Mattermost OIDC Mobile Bridge locations." fi fi @@ -1125,20 +1282,29 @@ if systemctl restart "$MM_BRIDGE_SERVICE" > "$log_file" 2>&1 ; then echo_ok else echo_failed - rollback_bridge - fatal "Failed to restart '${MM_BRIDGE_SERVICE}'." + if rollback_bridge ; then + fatal "Installation of Mattermost OIDC Mobile Bridge ${MM_NEW_VERSION} failed because '${MM_BRIDGE_SERVICE}' could not be restarted.\n The previous bridge installation was successfully restored." + else + fatal "Installation of Mattermost OIDC Mobile Bridge ${MM_NEW_VERSION} failed because '${MM_BRIDGE_SERVICE}' could not be restarted.\n The previous installation could not be restored.\n\n Manual intervention is required." + fi fi if ! bridge_test ; then - rollback_bridge - fatal "Mattermost OIDC Mobile Bridge functional test failed." + if rollback_bridge ; then + fatal "Installation of Mattermost OIDC Mobile Bridge ${MM_NEW_VERSION} failed.\n The previous bridge installation was successfully restored." + else + fatal "Installation of Mattermost OIDC Mobile Bridge ${MM_NEW_VERSION} failed\n and the previous installation could not be restored.\n\n Manual intervention is required." + fi fi if [[ -n "$NGINX_CONFIG_FILE" ]]; then if [[ -n "$MM_SITE_URL" ]]; then if ! nginx_e2e_test ; then - rollback_bridge - fatal "Mattermost OIDC Mobile Bridge end-to-end test through nginx failed." + if rollback_bridge ; then + fatal "Installation of Mattermost OIDC Mobile Bridge ${MM_NEW_VERSION} failed during the nginx end-to-end test.\n The previous bridge installation was successfully restored." + else + fatal "Installation of Mattermost OIDC Mobile Bridge ${MM_NEW_VERSION} failed during the nginx end-to-end test\n and the previous installation could not be restored.\n\n Manual intervention is required." + fi fi else warn "Mattermost SiteURL could not be read from '${MM_CONFIG_FILE}'. End-to-end nginx tests were skipped." @@ -1146,6 +1312,27 @@ if [[ -n "$NGINX_CONFIG_FILE" ]]; then fi +# ---------- +# Remove redundant same-version backup after successful tests +# ---------- + +if [[ -n "$REINSTALL_BRIDGE_BACKUP" && -f "$REINSTALL_BRIDGE_BACKUP" ]]; then + if cmp -s "$REINSTALL_BRIDGE_BACKUP" "$MM_BRIDGE_BIN"; then + echononl "Remove identical same-version bridge backup.." + if rm -f "$REINSTALL_BRIDGE_BACKUP" > "$log_file" 2>&1 ; then + echo_ok + REINSTALL_BRIDGE_BACKUP="" + else + echo_failed + warn "Could not remove redundant bridge backup '${REINSTALL_BRIDGE_BACKUP}'." + fi + else + echononl "Keep different same-version bridge backup.." + echo_ok + fi +fi + + # ---------- # Final information # ----------