|
1 | 1 | #!/bin/bash |
2 | 2 |
|
| 3 | +GUI_PROGRESS_LOG_FILE="" |
| 4 | +GUI_PROGRESS_SERVER_PID="" |
| 5 | +GUI_PROGRESS_URL="" |
| 6 | +GUI_PROGRESS_LOGGING_ACTIVE="false" |
| 7 | + |
| 8 | +gui_append_exit_trap() { |
| 9 | + local new_cmd="$1" |
| 10 | + local existing_trap |
| 11 | + existing_trap=$(trap -p EXIT | sed -n "s/^trap -- '\(.*\)' EXIT$/\1/p") |
| 12 | + if [ -n "$existing_trap" ]; then |
| 13 | + trap "$existing_trap"$'\n'"$new_cmd" EXIT |
| 14 | + else |
| 15 | + trap "$new_cmd" EXIT |
| 16 | + fi |
| 17 | +} |
| 18 | + |
| 19 | +gui_cleanup_progress_server() { |
| 20 | + if [ -n "$GUI_PROGRESS_SERVER_PID" ]; then |
| 21 | + if kill -0 "$GUI_PROGRESS_SERVER_PID" 2>/dev/null; then |
| 22 | + kill "$GUI_PROGRESS_SERVER_PID" 2>/dev/null || true |
| 23 | + wait "$GUI_PROGRESS_SERVER_PID" 2>/dev/null || true |
| 24 | + fi |
| 25 | + GUI_PROGRESS_SERVER_PID="" |
| 26 | + fi |
| 27 | + |
| 28 | + if [ -n "$GUI_PROGRESS_LOG_FILE" ] && [ -f "$GUI_PROGRESS_LOG_FILE" ]; then |
| 29 | + rm -f "$GUI_PROGRESS_LOG_FILE" 2>/dev/null || true |
| 30 | + fi |
| 31 | +} |
| 32 | + |
| 33 | +gui_launch_progress_server() { |
| 34 | + local bind_addr="$1" |
| 35 | + local gui_port="$2" |
| 36 | + local log_file="$3" |
| 37 | + |
| 38 | + if [ -z "$bind_addr" ] || [ -z "$gui_port" ] || [ -z "$log_file" ]; then |
| 39 | + return 1 |
| 40 | + fi |
| 41 | + |
| 42 | + if ! command -v python3 >/dev/null 2>&1; then |
| 43 | + echo "Warning: Live progress UI requires python3." >&2 |
| 44 | + return 1 |
| 45 | + fi |
| 46 | + |
| 47 | + python3 - "$bind_addr" "$gui_port" "$log_file" <<'PYTHON' & |
| 48 | +import http.server |
| 49 | +import socketserver |
| 50 | +import sys |
| 51 | +from pathlib import Path |
| 52 | +
|
| 53 | +BIND_ADDR = sys.argv[1] |
| 54 | +PORT = int(sys.argv[2]) |
| 55 | +LOG_PATH = Path(sys.argv[3]) |
| 56 | +
|
| 57 | +PROGRESS_PAGE = """<!DOCTYPE html> |
| 58 | +<html lang=\"en\"> |
| 59 | +<head> |
| 60 | +<meta charset=\"utf-8\"> |
| 61 | +<title>setup-k8s progress</title> |
| 62 | +<style> |
| 63 | +body { font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif; background: #0f172a; color: #e2e8f0; margin: 0; } |
| 64 | +.wrap { max-width: 900px; margin: 0 auto; padding: 32px; } |
| 65 | +.panel { background: rgba(15,23,42,0.75); backdrop-filter: blur(4px); border-radius: 16px; padding: 32px; box-shadow: 0 25px 50px rgba(15,23,42,0.35); } |
| 66 | +h1 { margin-top: 0; font-weight: 600; } |
| 67 | +.status { margin-bottom: 16px; font-size: 15px; color: #cbd5f5; } |
| 68 | +.log-shell { background: #020617; border-radius: 12px; border: 1px solid rgba(148,163,184,0.35); padding: 16px; height: 60vh; overflow: auto; } |
| 69 | +.log-shell pre { margin: 0; font-family: SFMono-Regular,Consolas,monospace; font-size: 13px; line-height: 1.5; color: #f1f5f9; } |
| 70 | +.hint { margin-top: 18px; font-size: 13px; color: #94a3b8; } |
| 71 | +</style> |
| 72 | +</head> |
| 73 | +<body> |
| 74 | +<div class=\"wrap\"> |
| 75 | + <div class=\"panel\"> |
| 76 | + <h1>setup-k8s Installation Progress</h1> |
| 77 | + <p class=\"status\" id=\"status\">Waiting for installer output...</p> |
| 78 | + <div class=\"log-shell\"><pre id=\"log\"></pre></div> |
| 79 | + <p class=\"hint\">Keep this tab open while the installer runs. Logs also appear in the terminal.</p> |
| 80 | + </div> |
| 81 | +</div> |
| 82 | +<script> |
| 83 | +const logEl = document.getElementById('log'); |
| 84 | +const statusEl = document.getElementById('status'); |
| 85 | +let autoScroll = true; |
| 86 | +let consecutiveFailures = 0; |
| 87 | +
|
| 88 | +function updateAutoScroll() { |
| 89 | + autoScroll = (logEl.scrollTop + logEl.clientHeight) >= (logEl.scrollHeight - 4); |
| 90 | +} |
| 91 | +
|
| 92 | +logEl.addEventListener('scroll', updateAutoScroll); |
| 93 | +
|
| 94 | +async function fetchLog() { |
| 95 | + try { |
| 96 | + const resp = await fetch('progress-log?ts=' + Date.now()); |
| 97 | + const text = await resp.text(); |
| 98 | + logEl.textContent = text; |
| 99 | + if (text.trim().length === 0) { |
| 100 | + statusEl.textContent = 'Waiting for installer output...'; |
| 101 | + } else { |
| 102 | + statusEl.textContent = 'Live log stream from setup-k8s'; |
| 103 | + } |
| 104 | + if (autoScroll) { |
| 105 | + logEl.scrollTop = logEl.scrollHeight; |
| 106 | + } |
| 107 | + consecutiveFailures = 0; |
| 108 | + } catch (err) { |
| 109 | + consecutiveFailures += 1; |
| 110 | + if (consecutiveFailures > 3) { |
| 111 | + statusEl.textContent = 'Installer finished or connection lost. Check the terminal for final status.'; |
| 112 | + } else { |
| 113 | + statusEl.textContent = 'Reconnecting to installer...'; |
| 114 | + } |
| 115 | + } finally { |
| 116 | + setTimeout(fetchLog, 1500); |
| 117 | + } |
| 118 | +} |
| 119 | +
|
| 120 | +fetchLog(); |
| 121 | +</script> |
| 122 | +</body> |
| 123 | +</html>""" |
| 124 | +
|
| 125 | +
|
| 126 | +class ProgressHandler(http.server.BaseHTTPRequestHandler): |
| 127 | + def send_body(self, body, *, status=200, content_type="text/html; charset=utf-8", head_only=False): |
| 128 | + data = body.encode("utf-8") |
| 129 | + self.send_response(status) |
| 130 | + self.send_header("Content-Type", content_type) |
| 131 | + self.send_header("Cache-Control", "no-store") |
| 132 | + self.send_header("Content-Length", str(len(data) if not head_only else 0)) |
| 133 | + self.end_headers() |
| 134 | + if not head_only: |
| 135 | + self.wfile.write(data) |
| 136 | +
|
| 137 | + def do_HEAD(self): |
| 138 | + if self.path.startswith("/progress-log") or self.path == "/healthz": |
| 139 | + self.send_body("", content_type="text/plain; charset=utf-8", head_only=True) |
| 140 | + elif self.path == "/favicon.ico": |
| 141 | + self.send_response(204) |
| 142 | + self.end_headers() |
| 143 | + else: |
| 144 | + self.send_body(PROGRESS_PAGE, head_only=True) |
| 145 | +
|
| 146 | + def do_GET(self): |
| 147 | + if self.path.startswith("/progress-log"): |
| 148 | + try: |
| 149 | + body = LOG_PATH.read_text(encoding="utf-8", errors="ignore") |
| 150 | + except OSError: |
| 151 | + body = "" |
| 152 | + self.send_body(body, content_type="text/plain; charset=utf-8") |
| 153 | + elif self.path == "/healthz": |
| 154 | + self.send_body("ok", content_type="text/plain; charset=utf-8") |
| 155 | + elif self.path == "/favicon.ico": |
| 156 | + self.send_response(204) |
| 157 | + self.end_headers() |
| 158 | + else: |
| 159 | + self.send_body(PROGRESS_PAGE) |
| 160 | +
|
| 161 | + def log_message(self, fmt, *args): |
| 162 | + sys.stderr.write("%s - - [%s] %s\n" % (self.address_string(), self.log_date_time_string(), fmt % args)) |
| 163 | +
|
| 164 | +
|
| 165 | +class ThreadingServer(socketserver.ThreadingMixIn, http.server.HTTPServer): |
| 166 | + daemon_threads = True |
| 167 | + allow_reuse_address = True |
| 168 | +
|
| 169 | +
|
| 170 | +def main(): |
| 171 | + try: |
| 172 | + httpd = ThreadingServer((BIND_ADDR, PORT), ProgressHandler) |
| 173 | + except OSError as exc: |
| 174 | + sys.stderr.write(f"Failed to start progress viewer: {exc}\n") |
| 175 | + sys.exit(2) |
| 176 | +
|
| 177 | + try: |
| 178 | + sys.stderr.write(f"Progress viewer listening on http://{BIND_ADDR}:{PORT}/progress\n") |
| 179 | + httpd.serve_forever() |
| 180 | + except KeyboardInterrupt: |
| 181 | + pass |
| 182 | + finally: |
| 183 | + httpd.server_close() |
| 184 | +
|
| 185 | +
|
| 186 | +if __name__ == "__main__": |
| 187 | + main() |
| 188 | +PYTHON |
| 189 | + GUI_PROGRESS_SERVER_PID=$! |
| 190 | + sleep 0.2 |
| 191 | + if ! kill -0 "$GUI_PROGRESS_SERVER_PID" 2>/dev/null; then |
| 192 | + echo "Warning: Failed to launch live progress server." >&2 |
| 193 | + GUI_PROGRESS_SERVER_PID="" |
| 194 | + return 1 |
| 195 | + fi |
| 196 | + |
| 197 | + gui_append_exit_trap "gui_cleanup_progress_server" |
| 198 | + return 0 |
| 199 | +} |
| 200 | + |
| 201 | +gui_enable_progress_logging() { |
| 202 | + if [ -z "$GUI_PROGRESS_LOG_FILE" ] || [ ! -f "$GUI_PROGRESS_LOG_FILE" ]; then |
| 203 | + return |
| 204 | + fi |
| 205 | + if [ "$GUI_PROGRESS_LOGGING_ACTIVE" = "true" ]; then |
| 206 | + return |
| 207 | + fi |
| 208 | + GUI_PROGRESS_LOGGING_ACTIVE="true" |
| 209 | + exec > >(tee -a "$GUI_PROGRESS_LOG_FILE") 2>&1 |
| 210 | + if [ -n "$GUI_PROGRESS_URL" ]; then |
| 211 | + echo "Live GUI progress: $GUI_PROGRESS_URL" |
| 212 | + echo "The browser view refreshes automatically; keep this terminal open until completion." |
| 213 | + fi |
| 214 | +} |
| 215 | + |
3 | 216 | # Launch a lightweight web UI that collects installation options and |
4 | 217 | # maps them to the regular CLI variables. |
5 | 218 | run_gui_installer() { |
@@ -141,18 +354,59 @@ document.addEventListener('DOMContentLoaded', updateWorkerFields); |
141 | 354 | <button type=\"submit\">Start installation</button> |
142 | 355 | </div> |
143 | 356 | </form> |
144 | | - <p class=\"hint\">This local server stops automatically after submission.</p> |
| 357 | + <p class=\"hint\">After submitting, this tab switches to a live log view so you can track progress here or watch the terminal output.</p> |
145 | 358 | </div> |
146 | 359 | </body> |
147 | 360 | </html> |
148 | 361 | """ |
149 | 362 |
|
150 | 363 | SUCCESS_PAGE = """<!DOCTYPE html> |
151 | 364 | <html lang=\"en\"> |
152 | | -<head><meta charset=\"utf-8\"><title>setup-k8s</title> |
153 | | -<style>body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;background:#f4f5f7;color:#1f2933;display:flex;align-items:center;justify-content:center;height:100vh;} .card{background:#fff;padding:32px;border-radius:12px;box-shadow:0 10px 20px rgba(15,23,42,0.08);text-align:center;} a{color:#2563eb;text-decoration:none;}</style> |
| 365 | +<head> |
| 366 | +<meta charset=\"utf-8\"> |
| 367 | +<title>setup-k8s</title> |
| 368 | +<style> |
| 369 | +body { font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif; background: #f5f6fb; color: #0f172a; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0; } |
| 370 | +.card { background: #fff; padding: 32px; border-radius: 14px; box-shadow: 0 30px 60px rgba(15,23,42,0.15); width: 440px; max-width: 90%; text-align: center; } |
| 371 | +.card h2 { margin-top: 0; } |
| 372 | +.card p { margin-bottom: 12px; } |
| 373 | +.muted { font-size: 13px; color: #64748b; } |
| 374 | +.card a { color: #2563eb; text-decoration: none; font-weight: 600; } |
| 375 | +</style> |
154 | 376 | </head> |
155 | | -<body><div class=\"card\"><h2>Configuration received</h2><p>You can close this tab and return to the terminal.</p></div></body></html>""" |
| 377 | +<body> |
| 378 | + <div class=\"card\"> |
| 379 | + <h2>Installation starting</h2> |
| 380 | + <p>Keep this tab open to see live progress (you can also watch the terminal output).</p> |
| 381 | + <p><a id=\"progress-link\" href=\"#\">Opening progress view…</a></p> |
| 382 | + <p class=\"muted\">The page refreshes automatically once the log server is available.</p> |
| 383 | + </div> |
| 384 | + <script> |
| 385 | + (function() { |
| 386 | + var target = window.location.origin + '/progress'; |
| 387 | + var link = document.getElementById('progress-link'); |
| 388 | + link.href = target; |
| 389 | + link.textContent = target; |
| 390 | +
|
| 391 | + function tryOpen() { |
| 392 | + fetch(target, { method: 'HEAD' }) |
| 393 | + .then(function(resp) { |
| 394 | + if (resp.ok) { |
| 395 | + window.location.href = target; |
| 396 | + return; |
| 397 | + } |
| 398 | + setTimeout(tryOpen, 2000); |
| 399 | + }) |
| 400 | + .catch(function() { |
| 401 | + setTimeout(tryOpen, 2000); |
| 402 | + }); |
| 403 | + } |
| 404 | +
|
| 405 | + setTimeout(tryOpen, 1500); |
| 406 | + })(); |
| 407 | + </script> |
| 408 | +</body> |
| 409 | +</html>""" |
156 | 410 |
|
157 | 411 | FIELDS = [ |
158 | 412 | "NODE_TYPE", |
@@ -274,6 +528,32 @@ PYTHON |
274 | 528 | exit 1 |
275 | 529 | fi |
276 | 530 |
|
| 531 | + if GUI_PROGRESS_LOG_FILE=$(mktemp -t setup-k8s-progress-XXXXXX.log 2>/dev/null); then |
| 532 | + : > "$GUI_PROGRESS_LOG_FILE" |
| 533 | + local display_host="$bind_addr" |
| 534 | + local display_hint="" |
| 535 | + if [ -z "$display_host" ] || [ "$display_host" = "0.0.0.0" ]; then |
| 536 | + display_host="127.0.0.1" |
| 537 | + display_hint=" (listening on all interfaces; replace the host as needed)" |
| 538 | + elif [ "$display_host" = "::" ]; then |
| 539 | + display_host="[::1]" |
| 540 | + display_hint=" (listening on all interfaces; replace the host as needed)" |
| 541 | + elif [[ "$display_host" == *:* && "$display_host" != [* ]]; then |
| 542 | + display_host="[$display_host]" |
| 543 | + fi |
| 544 | + |
| 545 | + if gui_launch_progress_server "$bind_addr" "$gui_port" "$GUI_PROGRESS_LOG_FILE"; then |
| 546 | + GUI_PROGRESS_URL="http://${display_host}:${gui_port}/progress" |
| 547 | + echo "Live progress UI will be available at ${GUI_PROGRESS_URL}${display_hint}" >&2 |
| 548 | + else |
| 549 | + rm -f "$GUI_PROGRESS_LOG_FILE" 2>/dev/null || true |
| 550 | + GUI_PROGRESS_LOG_FILE="" |
| 551 | + GUI_PROGRESS_URL="" |
| 552 | + fi |
| 553 | + else |
| 554 | + echo "Warning: Unable to create progress log file for GUI view." >&2 |
| 555 | + fi |
| 556 | + |
277 | 557 | local gui_pod_network_cidr="" |
278 | 558 | local gui_service_cidr="" |
279 | 559 | local gui_api_addr="" |
|
0 commit comments