#!/usr/bin/env bash # # install.sh — installer con un comando solo per "OptiWize Onboarding Inbox" # (macOS e Linux; per Windows usare install.ps1). # # COSA FA QUESTO SCRIPT, IN CHIARO (leggilo prima di lanciarlo, e' normale # diffidare di uno script ricevuto per mail): # 1. Controlla che Docker sia installato e ACCESO (non basta installato). # 2. Scarica ("pull") l'immagine del pannello da un registro Docker # pubblico (ghcr.io). Se l'immagine e' privata, chiede un token di # accesso (vedi sezione CONFIGURAZIONE qui sotto) SOLO a schermo, # non lo scrive mai su disco ne' lo stampa. # 3. La prima volta genera una chiave segreta casuale (serve a cifrare le # password dei device salvate dal pannello) e la conserva in un file # sul tuo computer, fuori dal container: ai riavvii successivi la # trova e la riusa, cosi' i dati gia' salvati restano leggibili. # 4. Crea un volume Docker per il database e avvia il container. # 5. Apre il browser sul pannello. # Non installa nient'altro sul sistema, non modifica nient'altro fuori dalla # cartella indicata in SECRET_KEY_FILE piu' sotto e dalla configurazione di # Docker stesso (volume + container). # # Uso tipico (ricevuto per mail): # curl -fsSL | bash # # Rilanciarlo in un secondo momento AGGIORNA il pannello all'ultima immagine # senza perdere i dati (e' pensato per essere rilanciato quante volte serve). set -euo pipefail # ============================================================================= # CONFIGURAZIONE — unico punto da cambiare # ============================================================================= # # IMAGE_IS_PRIVATE: # true = il pacchetto Docker su GHCR e' privato. Lo script prova comunque # prima un download anonimo (potrebbe funzionare se nel frattempo # e' stato reso pubblico); se fallisce, chiede un token GitHub con # permesso "read:packages" per autenticarsi. # false = il pacchetto e' pubblico: download diretto, mai chiesta nessuna # credenziale. Cambiare qui quando il repository/pacchetto GHCR # viene reso pubblico, e non serve toccare nient'altro nello script. IMAGE_IS_PRIVATE=false # Immagine da usare. Sovrascrivibile con la variabile d'ambiente INBOX_IMAGE, # utile sia per testare un'immagine locale (INBOX_IMAGE=optiwize-onboarding-inbox:test) # sia in produzione per puntare a un tag specifico invece di "latest". IMAGE="${INBOX_IMAGE:-ghcr.io/evoseed/optiwize-onboarding-inbox:latest}" CONTAINER_NAME="optiwize-onboarding-inbox" VOLUME_NAME="optiwize_inbox_data" CONTAINER_PORT=8080 # porta interna del pannello nel container (fissa) HOST_PORT_START=8099 # prima porta candidata sulla macchina dell'utente HOST_PORT_SCAN_TRIES=20 # quante porte successive provare se occupata # File in cui viene conservata la INBOX_SECRET_KEY generata al primo avvio. # Deve restare stabile fra un riavvio e l'altro: cambiarla rende illeggibili # le password dei device gia' salvate nel database. SECRET_KEY_DIR="${HOME}/.optiwize-onboarding-inbox" SECRET_KEY_FILE="${SECRET_KEY_DIR}/secret.key" # ============================================================================= # Utilita' di stampa # ============================================================================= # Tutti i messaggi diagnostici vanno su stderr: alcune funzioni (es. # resolve_secret_key, find_host_port) restituiscono il proprio risultato su # stdout tramite "$(...)" ed e' fondamentale che nessun testo informativo # finisca mescolato a quel valore. info() { printf '%s\n' "$*" >&2; } warn() { printf '[ATTENZIONE] %s\n' "$*" >&2; } fail() { printf '\n[ERRORE] %s\n' "$*" >&2; exit 1; } step() { printf '\n==> %s\n' "$*" >&2; } OS_NAME="$(uname -s 2>/dev/null || echo sconosciuto)" # ============================================================================= # 1. Docker installato e acceso # ============================================================================= docker_install_hint() { case "$OS_NAME" in Darwin) cat <<'EOF' Docker non risulta installato su questo Mac. Cosa fare: 1. Scarica Docker Desktop da: https://www.docker.com/products/docker-desktop/ 2. Installalo trascinando l'app in "Applicazioni", come qualsiasi app Mac. 3. Aprilo almeno una volta (Docker Desktop deve girare in background). 4. Rilancia questo comando. EOF ;; Linux) cat <<'EOF' Docker non risulta installato su questa macchina Linux. Cosa fare: 1. Segui le istruzioni ufficiali per la tua distribuzione: https://docs.docker.com/engine/install/ 2. Dopo l'installazione, aggiungi il tuo utente al gruppo "docker" per non dover usare sudo ogni volta: sudo usermod -aG docker "$USER" poi disconnettiti e riconnettiti (o riavvia) perche' abbia effetto. 3. Rilancia questo comando. EOF ;; *) cat <<'EOF' Docker non risulta installato (o non e' stato possibile riconoscere il sistema operativo). Installa Docker seguendo le istruzioni ufficiali: https://docs.docker.com/get-docker/ poi rilancia questo comando. EOF ;; esac } docker_not_running_hint() { case "$OS_NAME" in Darwin) cat <<'EOF' Docker Desktop e' installato ma non e' avviato (e' la causa piu' comune di questo errore: Docker Desktop non parte da solo all'accensione del Mac). Cosa fare: 1. Apri Docker Desktop (Spotlight -> scrivi "Docker Desktop" -> Invio, oppure dalla cartella Applicazioni). 2. Aspetta che l'icona della balena in alto a destra smetta di animarsi: significa che Docker e' pronto. 3. Rilancia questo comando. EOF ;; Linux) cat <<'EOF' Docker e' installato ma il servizio non e' attivo. Cosa fare (una delle due, a seconda di come hai installato Docker): - Docker Engine (piu' comune su server/Linux): sudo systemctl start docker - Docker Desktop per Linux: aprilo dal menu applicazioni e aspetta che l'icona indichi che e' avviato. Se l'errore parla di "permission denied" invece che di servizio fermo, il tuo utente non e' nel gruppo docker: sudo usermod -aG docker "$USER" poi disconnettiti/riconnettiti. Poi rilancia questo comando. EOF ;; *) cat <<'EOF' Docker e' installato ma non risulta in esecuzione. Avvialo e rilancia questo comando. EOF ;; esac } check_docker() { step "Verifico Docker" if ! command -v docker >/dev/null 2>&1; then docker_install_hint exit 1 fi if ! docker info >/dev/null 2>&1; then docker_not_running_hint exit 1 fi info "Docker c'e' ed e' in esecuzione." } # ============================================================================= # 2. Pull dell'immagine (con eventuale login GHCR per immagine privata) # ============================================================================= # Legge un valore da terminale anche quando lo script arriva via # "curl | bash": in quel caso lo stdin dello script e' occupato dallo script # stesso, quindi si legge esplicitamente da /dev/tty. prompt_hidden() { local prompt="$1" local value="" if [ -r /dev/tty ]; then read -r -s -p "$prompt" value < /dev/tty printf '\n' >&2 else fail "Serve un token GHCR ma non c'e' un terminale interattivo disponibile. Imposta la variabile d'ambiente INBOX_GHCR_TOKEN (e INBOX_GHCR_USER) prima di lanciare lo script." fi printf '%s' "$value" } prompt_visible() { local prompt="$1" local value="" if [ -r /dev/tty ]; then read -r -p "$prompt" value < /dev/tty else fail "Serve lo username GitHub ma non c'e' un terminale interattivo disponibile. Imposta la variabile d'ambiente INBOX_GHCR_USER." fi printf '%s' "$value" } ghcr_login() { info "L'immagine e' privata: serve autenticarsi a ghcr.io con un token GitHub." info "Serve un Personal Access Token con permesso \"read:packages\" (nessun altro permesso)." info "Si crea da: https://github.com/settings/tokens" info "Il token NON verra' salvato su disco ne' mostrato a schermo." local user="${INBOX_GHCR_USER:-}" local token="${INBOX_GHCR_TOKEN:-}" # Parametro posizionale opzionale: usato solo se le variabili d'ambiente # non sono valorizzate. Attenzione: se passato come argomento sulla riga # di comando puo' restare nella cronologia della shell; preferire la # variabile d'ambiente o la richiesta interattiva. if [ -z "$token" ] && [ "${1:-}" != "" ]; then token="$1" fi if [ -z "$user" ]; then user="$(prompt_visible "Il tuo username GitHub: ")" fi if [ -z "$token" ]; then token="$(prompt_hidden "Token GitHub (read:packages, non verra' mostrato): ")" fi [ -n "$user" ] || fail "Username GitHub vuoto: impossibile continuare." [ -n "$token" ] || fail "Token vuoto: impossibile continuare." if ! printf '%s' "$token" | docker login ghcr.io -u "$user" --password-stdin >/dev/null 2>&1; then fail "Login a ghcr.io non riuscito. Controlla username e token (permesso read:packages, non scaduto)." fi info "Login a ghcr.io riuscito." } image_exists_locally() { docker image inspect "$IMAGE" >/dev/null 2>&1 } pull_image() { step "Scarico l'immagine del pannello (${IMAGE})" local errore if errore=$(docker pull "$IMAGE" 2>&1); then printf '%s\n' "$errore" return 0 fi printf '%s\n' "$errore" >&2 # Il registry distingue due guasti molto diversi: "unauthorized" vuol dire # che l'immagine non e' accessibile (privata, o nome sbagliato), e mandare # l'utente a controllare la rete lo fa cercare nel posto sbagliato. local non_autorizzato=false case "$errore" in *unauthorized*|*denied*|*"authentication required"*|*forbidden*) non_autorizzato=true ;; esac if [ "$IMAGE_IS_PRIVATE" = true ]; then warn "Il download diretto non ha funzionato: molto probabilmente perche' l'immagine e' privata." ghcr_login "${1:-}" info "Riprovo il download..." if docker pull "$IMAGE"; then return 0 fi if image_exists_locally; then warn "Il download non e' riuscito nemmeno dopo il login, ma l'immagine '${IMAGE}' e' gia' presente su questo computer da un'esecuzione precedente: la uso cosi' com'e' (potrebbe non essere l'ultima versione)." return 0 fi fail "Il download dell'immagine non e' riuscito nemmeno dopo il login. Verifica che il token abbia il permesso 'read:packages' e non sia scaduto, e che l'immagine '${IMAGE}' sia corretta." fi if image_exists_locally; then warn "Il download non e' riuscito, ma l'immagine '${IMAGE}' e' gia' presente su questo computer: la uso cosi' com'e' (potrebbe non essere l'ultima versione)." return 0 fi if [ "$non_autorizzato" = true ]; then fail "Il registry ha risposto 'non autorizzato' per l'immagine '${IMAGE}'. La connessione a Internet funziona: il problema e' che l'immagine non risulta pubblicamente accessibile. Non e' qualcosa che puoi risolvere da qui: segnala a chi ti ha inviato queste istruzioni che il pacchetto va reso pubblico, oppure chiedi le credenziali per scaricarlo." fi fail "Il download dell'immagine '${IMAGE}' non e' riuscito. Controlla la connessione a Internet e riprova. Se il problema persiste, contatta chi ti ha inviato queste istruzioni." } # ============================================================================= # 3. Chiave segreta: genera la prima volta, riusa sempre dopo # ============================================================================= # Ordine di priorita' (il punto piu' delicato di questo script): # 1. file locale gia' presente da un'esecuzione precedente -> riusa quella; # 2. nessun file ma un container con lo stesso nome esiste gia' -> recupera # la chiave dal container esistente invece di generarne una nuova (che # renderebbe illeggibili i dati gia' salvati) e la salva nel file; # 3. nessuno dei due -> genera una chiave nuova e la salva nel file. resolve_secret_key() { step "Preparo la chiave di cifratura (INBOX_SECRET_KEY)" if [ -s "$SECRET_KEY_FILE" ]; then info "Trovata una chiave gia' generata in precedenza, la riuso." cat "$SECRET_KEY_FILE" return 0 fi if docker inspect "$CONTAINER_NAME" >/dev/null 2>&1; then local existing existing="$(docker inspect --format '{{range .Config.Env}}{{println .}}{{end}}' "$CONTAINER_NAME" \ | sed -n 's/^INBOX_SECRET_KEY=//p' | head -n1)" if [ -n "$existing" ]; then warn "Il file con la chiave non c'era piu', ma ho trovato un container gia' avviato in precedenza: ne riuso la chiave invece di generarne una nuova (altrimenti i dati gia' salvati diventerebbero illeggibili)." mkdir -p "$SECRET_KEY_DIR" printf '%s' "$existing" > "$SECRET_KEY_FILE" chmod 600 "$SECRET_KEY_FILE" printf '%s' "$existing" return 0 fi fi info "Prima installazione: genero una nuova chiave casuale e la conservo per i prossimi avvii." mkdir -p "$SECRET_KEY_DIR" local new_key new_key="$(head -c 48 /dev/urandom | base64 | tr -d '\n')" [ -n "$new_key" ] || fail "Non sono riuscito a generare una chiave casuale (comando base64 non disponibile?)." printf '%s' "$new_key" > "$SECRET_KEY_FILE" chmod 600 "$SECRET_KEY_FILE" info "Chiave salvata in: $SECRET_KEY_FILE (non cancellarla: serve a rileggere i dati gia' salvati)." printf '%s' "$new_key" } # ============================================================================= # 4. Porta libera # ============================================================================= port_in_use() { local port="$1" # /dev/tcp e' una funzione built-in di bash: se la connessione riesce, # qualcosa sta gia' ascoltando su quella porta. if (exec 3<>"/dev/tcp/127.0.0.1/${port}") 2>/dev/null; then exec 3>&- 2>/dev/null || true exec 3<&- 2>/dev/null || true return 0 fi return 1 } find_host_port() { local preferred="$1" local port="$preferred" local tries=0 while [ "$tries" -lt "$HOST_PORT_SCAN_TRIES" ]; do if ! port_in_use "$port"; then printf '%s' "$port" return 0 fi port=$((port + 1)) tries=$((tries + 1)) done fail "Non ho trovato una porta libera tra ${preferred} e $((preferred + HOST_PORT_SCAN_TRIES - 1)). Libera una porta o modifica HOST_PORT_START nello script." } # ============================================================================= # 5. Avvio (gestisce anche l'aggiornamento se il container esiste gia') # ============================================================================= open_browser() { local url="$1" case "$OS_NAME" in Darwin) command -v open >/dev/null 2>&1 && open "$url" || true ;; Linux) command -v xdg-open >/dev/null 2>&1 && xdg-open "$url" >/dev/null 2>&1 || true ;; esac } wait_ready() { local port="$1" local tries=0 step "Attendo che il pannello risponda" while [ "$tries" -lt 30 ]; do if port_in_use "$port"; then info "Il pannello risponde." return 0 fi sleep 1 tries=$((tries + 1)) done warn "Il container e' partito ma il pannello non risponde ancora sulla porta ${port}. Controlla i log con: docker logs ${CONTAINER_NAME}" } main() { check_docker local existing_container=false local preferred_port="$HOST_PORT_START" if docker inspect "$CONTAINER_NAME" >/dev/null 2>&1; then existing_container=true local prev_port # "|| true": se il container esiste ma e' fermo, "docker port" non # restituisce nulla e fallisce (nessuna porta pubblicata mentre e' # fermo) — con "set -e"+"pipefail" attivi, senza questa protezione # l'intero script si fermerebbe qui in silenzio. In quel caso si # ripiega semplicemente su HOST_PORT_START. prev_port="$(docker port "$CONTAINER_NAME" "${CONTAINER_PORT}/tcp" 2>/dev/null | head -n1 | sed -E 's/.*:([0-9]+)$/\1/')" || true if [ -n "${prev_port:-}" ]; then preferred_port="$prev_port" fi fi local secret_key secret_key="$(resolve_secret_key)" pull_image "${1:-}" step "Preparo il volume dati" docker volume create "$VOLUME_NAME" >/dev/null info "Volume pronto: $VOLUME_NAME" # Il vecchio container va rimosso PRIMA di scegliere la porta: se non lo # si facesse, la sua stessa porta risulterebbe "occupata" (e' ancora lui # ad ascoltarci) e a ogni riavvio dello script la porta salirebbe di uno # invece di restare stabile. Il volume dati non e' toccato da questo rm: # vive separatamente dal container. if [ "$existing_container" = true ]; then step "Aggiorno il container esistente (il volume dati non viene toccato)" docker rm -f "$CONTAINER_NAME" >/dev/null else step "Avvio il container" fi local host_port host_port="$(find_host_port "$preferred_port")" if [ "$host_port" != "$preferred_port" ]; then warn "La porta ${preferred_port} risultava occupata: uso la porta ${host_port} al suo posto." fi docker run -d \ --name "$CONTAINER_NAME" \ --restart unless-stopped \ -p "${host_port}:${CONTAINER_PORT}" \ -e "INBOX_SECRET_KEY=${secret_key}" \ -v "${VOLUME_NAME}:/data" \ "$IMAGE" >/dev/null local url="http://localhost:${host_port}" wait_ready "$host_port" open_browser "$url" cat <