Ir al contenido

Replicar una compañía de producción a staging

Lleva un snapshot de la base CouchDB de una company desde producción a staging (sin replicación activa) y deja autorizados en staging a usuarios ya existentes, escribiendo en el Postgres de staging (companies + users.user_roles).

  • Caso de referencia: company Mavala (c1c8a8ff163c5b78) → autorizar a mvaldivieso@ticketplus.com y sorellana@ticketplus.com.
  • Tipo: operación manual one-shot. Idempotente donde es posible.
  • Riesgo: escribe en staging (CouchDB + Postgres). No toca producción (solo lee).
InputEjemploCómo obtenerlo
COMPANY_HEXc1c8a8ff163c5b78id de la company (hex, sin prefijo)
COMPANY_PG_IDCOMPANY#c1c8a8ff163c5b78id en tabla companies (con prefijo)
EMAILSmvaldivieso@…, sorellana@…usuarios ya registrados en staging
ROLES["admin","sys_admin"]confirmar con quien solicita
CTXticketplus/do-production-biteskubectl config get-contexts
  • (Si la recopia es limpia) DBs staging_<COMPANY_HEX>_* previas borradas (ver Fase 2.5).
  • DB CouchDB staging_<COMPANY_HEX>_default_tables-prod con el snapshot de prod.
  • DBs rolling _core / _daily_* regeneradas por el cron de staging desde tables-prod.
  • Fila <COMPANY_PG_ID> en Postgres staging companies.
  • Usuarios de EMAILS con user_roles que incluye "<COMPANY_HEX>": ROLES.
  • Acceso kubectl al contexto ticketplus/do-production-bites.
  • python3 local (para construir JSON/SQL con escaping seguro).
  • Conectividad de red entre namespaces couch (verificada en el paso de descubrimiento).
Ventana de terminal
CTX=ticketplus/do-production-bites
# CouchDB PROD admin (OJO: usar *-tables-admin-secret, NO el secret del chart Helm)
PU=$(kubectl --context $CTX -n couchdb get secret couchdb-tables-admin-secret -o jsonpath='{.data.username}' | base64 -d)
PP=$(kubectl --context $CTX -n couchdb get secret couchdb-tables-admin-secret -o jsonpath='{.data.password}' | base64 -d)
# CouchDB STAGING admin
SU=$(kubectl --context $CTX -n couchdb-staging get secret couchdb-tables-admin-secret-staging -o jsonpath='{.data.username}' | base64 -d)
SP=$(kubectl --context $CTX -n couchdb-staging get secret couchdb-tables-admin-secret-staging -o jsonpath='{.data.password}' | base64 -d)

Fase 2 — Verificación de estado inicial (recomendado)

Sección titulada «Fase 2 — Verificación de estado inicial (recomendado)»
Ventana de terminal
# 1) Confirmar nombre de la DB de la company en PROD
kubectl --context $CTX -n couchdb exec couchdb-couchdb-0 -c couchdb -- \
curl -s "http://$PU:$PP@127.0.0.1:5984/_all_dbs" | COMPANY_HEX="$COMPANY_HEX" python3 -c \
"import sys,json,os;print([d for d in json.load(sys.stdin) if os.environ['COMPANY_HEX'] in d])"
# -> testing_<COMPANY_HEX>_default_tables-prod
# 2) Confirmar estado de STAGING couch (solo sistema + companies default)
kubectl --context $CTX -n couchdb-staging exec couchdb-staging-couchdb-0 -c couchdb -- \
curl -s "http://$SU:$SP@127.0.0.1:5984/_all_dbs"

Para Postgres ver la Fase 5 (mismo patrón de pod efímero psql-probe).

Fase 2.5 — Limpieza previa de la company en staging (recopia limpia)

Sección titulada «Fase 2.5 — Limpieza previa de la company en staging (recopia limpia)»

Necesaria cuando la company ya existe en staging y se quiere un snapshot fresco (no un merge contra datos viejos). Con el modelo rolling-DB, una company en staging no es una sola DB: tiene varias y conviene borrarlas todas antes de recopiar.

DBs que puede tener una company en staging (todas con el prefijo staging_<HEX>_default):

DBOrigenNotas
…_tables-prodcopiada de prodúnica que se recopia desde prod
…_corela regenera el cron rolling-db de stagingwarm CORE
…_daily_YYYYMMDD (varias)idemuna por día activo
…_staging_<HEX>_default_daily_*bug de doble prefijobasura; limpiar de paso
Ventana de terminal
# 1) Listar TODO lo de esta company en staging (scope estricto al HEX) y su doc_count.
# Revisar SIEMPRE esta lista antes de borrar: solo deben aparecer DBs del HEX.
kubectl --context $CTX -n couchdb-staging exec couchdb-staging-couchdb-0 -c couchdb -- \
curl -s "http://$SU:$SP@127.0.0.1:5984/_all_dbs" | COMPANY_HEX="$COMPANY_HEX" python3 -c \
"import sys,json,os;h=os.environ['COMPANY_HEX'];print('\n'.join(d for d in json.load(sys.stdin) if h in d))" \
> /tmp/del_targets.txt
while read db; do
dc=$(kubectl --context $CTX -n couchdb-staging exec couchdb-staging-couchdb-0 -c couchdb -- \
curl -s "http://$SU:$SP@127.0.0.1:5984/$db" | python3 -c "import sys,json;print(json.load(sys.stdin).get('doc_count','?'))")
printf " %-70s doc_count=%s\n" "$db" "$dc"
done < /tmp/del_targets.txt
# 2) Borrar cada DB listada (idempotente: si no existe, CouchDB responde 404).
while read db; do
resp=$(kubectl --context $CTX -n couchdb-staging exec couchdb-staging-couchdb-0 -c couchdb -- \
curl -s -X DELETE "http://$SU:$SP@127.0.0.1:5984/$db")
printf " DELETE %-70s -> %s\n" "$db" "$resp" # -> {"ok":true}
done < /tmp/del_targets.txt
# 3) Verificar que no queda nada del HEX
kubectl --context $CTX -n couchdb-staging exec couchdb-staging-couchdb-0 -c couchdb -- \
curl -s "http://$SU:$SP@127.0.0.1:5984/_all_dbs" | COMPANY_HEX="$COMPANY_HEX" python3 -c \
"import sys,json,os;h=os.environ['COMPANY_HEX'];r=[d for d in json.load(sys.stdin) if h in d];print(r if r else '(ninguna -- OK)')"

Fase 3 — Snapshot CouchDB (one-shot, sin replicación activa)

Sección titulada «Fase 3 — Snapshot CouchDB (one-shot, sin replicación activa)»
Ventana de terminal
SRC=testing_${COMPANY_HEX}_default_tables-prod
DST=staging_${COMPANY_HEX}_default_tables-prod
STG_HOST=couchdb-staging-svc-couchdb.couchdb-staging.svc.cluster.local:5984
# Construir body con AMBOS lados como URL completa.
# IMPORTANTE: el source debe ir como URL a 127.0.0.1 (no como nombre de DB local),
# si no el replicador arma una self-URL con bind_address=any -> nxdomain.
BODY=$(PU="$PU" PP="$PP" SU="$SU" SP="$SP" STG_HOST="$STG_HOST" SRC="$SRC" DST="$DST" python3 << 'PY'
import os,json,urllib.parse as u
q=lambda s:u.quote(s,safe='')
src="http://"+q(os.environ["PU"])+":"+q(os.environ["PP"])+"@127.0.0.1:5984/"+os.environ["SRC"]
dst="http://"+q(os.environ["SU"])+":"+q(os.environ["SP"])+"@"+os.environ["STG_HOST"]+"/"+os.environ["DST"]
print(json.dumps({"source":src,"target":dst,"create_target":True,"continuous":False}))
PY
)
# Disparar replicacion desde el pod couch de PROD
echo "$BODY" | kubectl --context $CTX -n couchdb exec -i couchdb-couchdb-0 -c couchdb -- \
curl -s -X POST "http://$PU:$PP@127.0.0.1:5984/_replicate" \
-H "Content-Type: application/json" -d @- | python3 -m json.tool
# -> {"ok": true, ... "docs_written": N, "doc_write_failures": 0}
# _security NO se replica: aplicarlo aparte (igual que prod: admin-only)
kubectl --context $CTX -n couchdb-staging exec couchdb-staging-couchdb-0 -c couchdb -- \
curl -s -X PUT "http://$SU:$SP@127.0.0.1:5984/$DST/_security" \
-H "Content-Type: application/json" \
-d '{"members":{"roles":["_admin"]},"admins":{"roles":["_admin"]}}'
# Verificar destino
kubectl --context $CTX -n couchdb-staging exec couchdb-staging-couchdb-0 -c couchdb -- \
curl -s "http://$SU:$SP@127.0.0.1:5984/$DST" | python3 -c \
"import sys,json;d=json.load(sys.stdin);print('doc_count:',d['doc_count'])"
Ventana de terminal
launch_psql() { # $1 = namespace
local NS=$1
kubectl --context $CTX -n $NS delete pod psql-probe --ignore-not-found >/dev/null 2>&1
local DBURL=$(kubectl --context $CTX -n $NS get secret tables-socket-secrets -o jsonpath='{.data.DATABASE_URL}' | base64 -d)
kubectl --context $CTX -n $NS run psql-probe --image=postgres:16 --restart=Never \
--env="PGURL=$DBURL" --command -- sleep 900 >/dev/null 2>&1
kubectl --context $CTX -n $NS wait --for=condition=Ready pod/psql-probe --timeout=90s
}
psql_run() { # $1 = namespace, $2 = sql
kubectl --context $CTX -n $1 exec -i psql-probe -- bash -c 'psql "$PGURL" -X -A -F"\t" -v ON_ERROR_STOP=1'
}

Fase 5 — Postgres: copiar la company a staging

Sección titulada «Fase 5 — Postgres: copiar la company a staging»
Ventana de terminal
launch_psql tables-socket # prod
# Extraer la fila completa de la company desde PROD como JSON
kubectl --context $CTX -n tables-socket exec psql-probe -- bash -c \
'psql "$PGURL" -X -t -A -c "select row_to_json(c) from companies c where id = '"'"'COMPANY#'"$COMPANY_HEX"''"'"';"' \
> /tmp/company.json
launch_psql tables-socket-staging # staging
# Construir INSERT idempotente con escaping seguro
SQL=$(python3 << 'PY'
import json
d=json.load(open('/tmp/company.json'))
def lit(v):
if v is None: return 'NULL'
if isinstance(v,(int,float)): return str(v)
return "'"+str(v).replace("'","''")+"'"
cols=['id','name','description','address','phone','email','status','created','deleted_at','rut','image']
vals=', '.join(lit(d.get(c)) for c in cols)
print(f"INSERT INTO companies ({', '.join(cols)}) VALUES ({vals}) ON CONFLICT (id) DO NOTHING;")
PY
)
echo "$SQL" | psql_run tables-socket-staging # -> INSERT 0 1

Merge no destructivo (conserva otras companies del usuario):

Ventana de terminal
SQL="UPDATE users
SET user_roles = COALESCE(user_roles,'{}'::jsonb) || '{\"$COMPANY_HEX\":[\"admin\",\"sys_admin\"]}'::jsonb
WHERE email IN ('mvaldivieso@ticketplus.com','sorellana@ticketplus.com');"
echo "$SQL" | psql_run tables-socket-staging # -> UPDATE 2

Variante: dejar al usuario SOLO con la nueva company (desasocia las default y apunta también el campo deprecado company_id):

Ventana de terminal
SQL="UPDATE users
SET user_roles = '{\"$COMPANY_HEX\":[\"admin\",\"sys_admin\"]}'::jsonb,
company_id = '$COMPANY_HEX'
WHERE email IN ('mvaldivieso@ticketplus.com','sorellana@ticketplus.com');"
echo "$SQL" | psql_run tables-socket-staging # -> UPDATE 2
Ventana de terminal
kubectl --context $CTX -n tables-socket delete pod psql-probe --ignore-not-found
kubectl --context $CTX -n tables-socket-staging delete pod psql-probe --ignore-not-found
  • Eliminar los pods efímeros psql-probe.
  • Re-login de los usuarios en staging para refrescar el token con el nuevo user_roles.
Ventana de terminal
launch_psql tables-socket-staging
echo "select email, company_id, user_roles from users
where email in ('mvaldivieso@ticketplus.com','sorellana@ticketplus.com');" | psql_run tables-socket-staging

Esperado: cada usuario con la clave "<COMPANY_HEX>": ["admin","sys_admin"] en user_roles, y la DB staging_<COMPANY_HEX>_default_tables-prod visible en /_all_dbs de staging.

SíntomaCausaSolución
unauthorized / Name or password is incorrect en couchse usó el secret del chart Helmusar couchdb-tables-admin-secret(-staging)
_replicate con http://any:5984/_session + nxdomainsource dado como nombre de DB local; el replicador arma self-URL con bind_address=anypasar el source como URL completa a 127.0.0.1 con credenciales
_replicate con invalid UTF-8 JSONescaping roto al construir el body en shellconstruir el JSON con python3 (heredoc), no inline
replication_auth_error con objeto auth.basicversión de CouchDB no parsea esa formacredenciales URL-encoded embebidas en la URL
container not found ("psql-probe")el sleep del pod expirórecrear con launch_psql
Postgres inaccesible desde localhost privado del VPCsiempre vía pod efímero in-cluster
DBs de doble prefijo (staging_<HEX>_default_staging_<HEX>_default_daily_*)bug previo del rolling-db al componer el nombrebasura; se limpian en la Fase 2.5 (entran por el filtro h in d)
recopia mezcla datos viejos con el snapshotno se hizo limpieza previacorrer la Fase 2.5 antes del snapshot
  • Snapshot, no replicación activa: continuous:false. No deja _replicator doc.
  • Prefijo de entorno: prod usa testing_, staging usa staging_. Se cambia solo el prefijo; <COMPANY_HEX> y sufijo _default_tables-prod se conservan.
  • _security no viaja en la replicación; replicarlo con un PUT explícito.
  • Discrepancias menores de doc_count (el origen es un cluster de 2 nodos) son esperables; lo determinante es doc_write_failures: 0 y source_last_seq consumido entero.
  • Limpieza previa scopeada por hex (Fase 2.5): el modelo rolling-db crea varias DBs por company (_core, _daily_*, _tables-prod) más posibles malformadas de doble prefijo. Sólo tables-prod se recopia desde prod; el resto lo regenera el cron de staging. Nunca borrar con globs amplios (staging_*_tables-prod) que cruzan companies.

Para entender el modelo rolling-db ver Deploy de Rolling Databases a staging y Arquitectura.