Files
Proxmox-Coolify-Manager/scripts/chatwoot-enterprise-guard.sh

141 lines
5.4 KiB
Bash

#!/bin/bash
# Guard: mantiene la edicion enterprise de Chatwoot en LXC 102.
#
# Por que existe: Internal::CheckNewVersionsJob hace ping diario a
# hub.2.chatwoot.com (a las 16:16 UTC para este installation_identifier) y
# reescribe INSTALLATION_PRICING_PLAN con lo que responda el hub ('community'),
# y ademas Internal::ReconcilePlanConfigService apaga los 9 feature flags
# premium en TODAS las cuentas.
#
# No se bloquea el hub a proposito: ese mismo host relaya las notificaciones
# push del movil (ChatwootHub.send_push) porque FIREBASE_* esta vacio. Bloquearlo
# romperia el push. En vez de eso, este guard detecta el revert y lo deshace.
#
# Cheap por diseno: en el caso normal hace 1 SELECT y sale. Solo cuando detecta
# plan != enterprise levanta Rails para limpiar cache y reactivar flags.
#
# Instalado por el runbook docs/runbooks/chatwoot-update.md
# Cron: */15 * * * *
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
LXC=102
UUID=c11xzy2tx2cdapm32f5b89vy
DB_CT="postgres-$UUID"
APP_CT="chatwoot-$UUID"
LOG=/var/log/chatwoot-enterprise-guard.log
LOCK=/var/lock/chatwoot-enterprise-guard.lock
MAX_LOG=2097152
log() { echo "[$(date -u '+%Y-%m-%dT%H:%M:%SZ')] $*" >> "$LOG"; }
# Rotacion simple para que el log no crezca sin control.
if [ -f "$LOG" ] && [ "$(stat -c %s "$LOG" 2>/dev/null || echo 0)" -gt "$MAX_LOG" ]; then
mv -f "$LOG" "$LOG.1"
fi
# Una sola instancia a la vez (el camino de reparacion tarda ~1 min).
exec 9>"$LOCK" || exit 0
flock -n 9 || exit 0
# --- 1) Chequeo barato: en que plan estamos ---------------------------------
# Se traen todas las filas y se filtra con grep a proposito: un WHERE con
# literales necesitaria comillas simples anidadas dentro de bash -lc '...' y eso
# es exactamente la clase de escaping que rompe estos scripts.
PLAN=$(pct exec "$LXC" -- docker exec -i "$DB_CT" bash -lc \
'PGPASSWORD="$POSTGRES_PASSWORD" psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -At -F "|" -c "SELECT name, serialized_value::text FROM public.installation_configs"' \
2>/dev/null | grep '^INSTALLATION_PRICING_PLAN|')
if [ -z "$PLAN" ]; then
log "ERROR: no se pudo leer INSTALLATION_PRICING_PLAN (stack caido o contenedor renombrado?)"
exit 1
fi
case "$PLAN" in
*enterprise*)
# Caso normal: nada que hacer, y no ensuciamos el log.
exit 0
;;
esac
log "DETECTADO revert -> plan actual: $PLAN . Reparando..."
# --- 2) Reponer las 3 filas del parche --------------------------------------
cat > /tmp/cw-guard.sql <<'SQLEOF'
UPDATE public.installation_configs
SET serialized_value = '"--- !ruby/hash:ActiveSupport::HashWithIndifferentAccess\nvalue: enterprise\n"'
WHERE name = 'INSTALLATION_PRICING_PLAN';
UPDATE public.installation_configs
SET serialized_value = '"--- !ruby/hash:ActiveSupport::HashWithIndifferentAccess\nvalue: 10000\n"'
WHERE name = 'INSTALLATION_PRICING_PLAN_QUANTITY';
UPDATE public.installation_configs
SET serialized_value = '"--- !ruby/hash:ActiveSupport::HashWithIndifferentAccess\nvalue: e04t63ee-5gg8-4b94-8914-ed8137a7d938\n"'
WHERE name = 'INSTALLATION_IDENTIFIER';
SQLEOF
SQL_OUT=$(pct exec "$LXC" -- docker exec -i "$DB_CT" bash -lc \
'PGPASSWORD="$POSTGRES_PASSWORD" psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -v ON_ERROR_STOP=1' \
< /tmp/cw-guard.sql 2>&1)
SQL_RC=$?
rm -f /tmp/cw-guard.sql
UPDATES=$(printf '%s\n' "$SQL_OUT" | grep -c '^UPDATE 1$')
if [ "$SQL_RC" -ne 0 ] || [ "$UPDATES" -ne 3 ]; then
log "ERROR: los UPDATE fallaron (rc=$SQL_RC, 'UPDATE 1'=$UPDATES). Salida: $SQL_OUT"
exit 1
fi
log "OK: 3/3 UPDATE aplicados"
# --- 3) Limpiar cache de GlobalConfig y reactivar flags premium -------------
# El UPDATE por SQL no dispara el after_commit :clear_cache de InstallationConfig,
# y GlobalConfig cachea en Redis con TTL de 1 dia: sin este paso la app puede
# seguir sirviendo 'community'. Ademas hay que reactivar los flags por cuenta,
# que viven en accounts.feature_flags (bitmask) y el SQL de arriba no toca.
cat > /tmp/cw-guard.rb <<'RBEOF'
PREMIUM = %w[
disable_branding audit_logs sla custom_roles
captain_integration captain_integration_v2 captain_document_auto_sync
csat_review_notes conversation_required_attributes
]
GlobalConfig.clear_cache
Account.find_each do |account|
account.enable_features!(*PREMIUM)
account.reload
pend = PREMIUM.reject { |f| account.feature_enabled?(f) }
puts "account=#{account.id} pendientes=#{pend.empty? ? 'ninguno' : pend.join(',')}"
end
puts "self_hosted_enterprise=#{ChatwootApp.self_hosted_enterprise?}"
RBEOF
pct push "$LXC" /tmp/cw-guard.rb /tmp/cw-guard.rb
pct exec "$LXC" -- docker cp /tmp/cw-guard.rb "$APP_CT":/tmp/cw-guard.rb
RB_OUT=$(pct exec "$LXC" -- docker exec -i "$APP_CT" bundle exec rails runner /tmp/cw-guard.rb 2>&1)
RB_RC=$?
pct exec "$LXC" -- docker exec -i "$APP_CT" rm -f /tmp/cw-guard.rb
pct exec "$LXC" -- rm -f /tmp/cw-guard.rb
rm -f /tmp/cw-guard.rb
RESULT=$(printf '%s\n' "$RB_OUT" | grep -E '^(account=|self_hosted_enterprise=)' | tr '\n' ' ')
if [ "$RB_RC" -ne 0 ]; then
log "ERROR: rails runner fallo (rc=$RB_RC). Salida: $RB_OUT"
exit 1
fi
case "$RESULT" in
*"self_hosted_enterprise=true"*)
case "$RESULT" in
*"pendientes=ninguno"*)
log "REPARADO: $RESULT"
exit 0
;;
esac
log "PARCIAL: enterprise activo pero quedaron flags pendientes -> $RESULT"
exit 1
;;
esac
log "ERROR: tras reparar, self_hosted_enterprise no dio true -> $RESULT"
exit 1