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 amvaldivieso@ticketplus.comysorellana@ticketplus.com. - Tipo: operación manual one-shot. Idempotente donde es posible.
- Riesgo: escribe en staging (CouchDB + Postgres). No toca producción (solo lee).
Inputs del proceso
Sección titulada «Inputs del proceso»| Input | Ejemplo | Cómo obtenerlo |
|---|---|---|
COMPANY_HEX | c1c8a8ff163c5b78 | id de la company (hex, sin prefijo) |
COMPANY_PG_ID | COMPANY#c1c8a8ff163c5b78 | id en tabla companies (con prefijo) |
EMAILS | mvaldivieso@…, sorellana@… | usuarios ya registrados en staging |
ROLES | ["admin","sys_admin"] | confirmar con quien solicita |
CTX | ticketplus/do-production-bites | kubectl config get-contexts |
Output final esperado
Sección titulada «Output final esperado»- (Si la recopia es limpia) DBs
staging_<COMPANY_HEX>_*previas borradas (ver Fase 2.5). - DB CouchDB
staging_<COMPANY_HEX>_default_tables-prodcon el snapshot de prod. - DBs rolling
_core/_daily_*regeneradas por el cron de staging desdetables-prod. - Fila
<COMPANY_PG_ID>en Postgres stagingcompanies. - Usuarios de
EMAILSconuser_rolesque incluye"<COMPANY_HEX>": ROLES.
Prerrequisitos
Sección titulada «Prerrequisitos»- Acceso
kubectlal contextoticketplus/do-production-bites. python3local (para construir JSON/SQL con escaping seguro).- Conectividad de red entre namespaces couch (verificada en el paso de descubrimiento).
Fase 1 — Credenciales
Sección titulada «Fase 1 — Credenciales»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 adminSU=$(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)»# 1) Confirmar nombre de la DB de la company en PRODkubectl --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):
| DB | Origen | Notas |
|---|---|---|
…_tables-prod | copiada de prod | única que se recopia desde prod |
…_core | la regenera el cron rolling-db de staging | warm CORE |
…_daily_YYYYMMDD (varias) | idem | una por día activo |
…_staging_<HEX>_default_daily_* | bug de doble prefijo | basura; limpiar de paso |
# 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.txtwhile 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 HEXkubectl --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)»SRC=testing_${COMPANY_HEX}_default_tables-prodDST=staging_${COMPANY_HEX}_default_tables-prodSTG_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 uq=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 PRODecho "$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 destinokubectl --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'])"Fase 4 — Pod efímero psql para Postgres
Sección titulada «Fase 4 — Pod efímero psql para Postgres»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»launch_psql tables-socket # prod# Extraer la fila completa de la company desde PROD como JSONkubectl --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 seguroSQL=$(python3 << 'PY'import jsond=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 1Fase 6 — Postgres: autorizar usuarios
Sección titulada «Fase 6 — Postgres: autorizar usuarios»Merge no destructivo (conserva otras companies del usuario):
SQL="UPDATE usersSET user_roles = COALESCE(user_roles,'{}'::jsonb) || '{\"$COMPANY_HEX\":[\"admin\",\"sys_admin\"]}'::jsonbWHERE email IN ('mvaldivieso@ticketplus.com','sorellana@ticketplus.com');"echo "$SQL" | psql_run tables-socket-staging # -> UPDATE 2Variante: dejar al usuario SOLO con la nueva company (desasocia las default y apunta
también el campo deprecado company_id):
SQL="UPDATE usersSET 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 2Fase 7 — Cierre
Sección titulada «Fase 7 — Cierre»kubectl --context $CTX -n tables-socket delete pod psql-probe --ignore-not-foundkubectl --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.
Verificación final
Sección titulada «Verificación final»launch_psql tables-socket-stagingecho "select email, company_id, user_roles from users where email in ('mvaldivieso@ticketplus.com','sorellana@ticketplus.com');" | psql_run tables-socket-stagingEsperado: 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.
Troubleshooting
Sección titulada «Troubleshooting»| Síntoma | Causa | Solución |
|---|---|---|
unauthorized / Name or password is incorrect en couch | se usó el secret del chart Helm | usar couchdb-tables-admin-secret(-staging) |
_replicate con http://any:5984/_session + nxdomain | source dado como nombre de DB local; el replicador arma self-URL con bind_address=any | pasar el source como URL completa a 127.0.0.1 con credenciales |
_replicate con invalid UTF-8 JSON | escaping roto al construir el body en shell | construir el JSON con python3 (heredoc), no inline |
replication_auth_error con objeto auth.basic | versión de CouchDB no parsea esa forma | credenciales URL-encoded embebidas en la URL |
container not found ("psql-probe") | el sleep del pod expiró | recrear con launch_psql |
| Postgres inaccesible desde local | host privado del VPC | siempre 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 nombre | basura; se limpian en la Fase 2.5 (entran por el filtro h in d) |
| recopia mezcla datos viejos con el snapshot | no se hizo limpieza previa | correr la Fase 2.5 antes del snapshot |
Notas de diseño
Sección titulada «Notas de diseño»- Snapshot, no replicación activa:
continuous:false. No deja_replicatordoc. - Prefijo de entorno: prod usa
testing_, staging usastaging_. Se cambia solo el prefijo;<COMPANY_HEX>y sufijo_default_tables-prodse conservan. _securityno viaja en la replicación; replicarlo con unPUTexplícito.- Discrepancias menores de
doc_count(el origen es un cluster de 2 nodos) son esperables; lo determinante esdoc_write_failures: 0ysource_last_seqconsumido 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ólotables-prodse 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.