staging: artefactos de despliegue dockerizado + neutralizacion de datos
- .meteorignore excluye scripts/ del bundle Meteor (mongosh usa global db); docker-compose.staging.yml (mongo7/redis/notif-shadow/mailhog); scripts/neutralize-contacts.mongo.js (contactos + cola de correo); secrets/*.staging.*.example
This commit is contained in:
parent
1ef0012af9
commit
2e298f9ee5
5 changed files with 346 additions and 0 deletions
|
|
@ -3,3 +3,5 @@ cucumber
|
||||||
output.json
|
output.json
|
||||||
# REST smoke-test harness runs standalone (Node), never as Meteor server code
|
# REST smoke-test harness runs standalone (Node), never as Meteor server code
|
||||||
smoke
|
smoke
|
||||||
|
# Ops/deploy scripts (p.ej. scripts/*.mongo.js usan el global `db` de mongosh, no de Meteor)
|
||||||
|
scripts
|
||||||
|
|
|
||||||
163
docker-compose.staging.yml
Normal file
163
docker-compose.staging.yml
Normal file
|
|
@ -0,0 +1,163 @@
|
||||||
|
# Stack de STAGING de "Todos contra el fuego" para testfuegos.comunes.org.
|
||||||
|
# Derivado de docker-compose.yml (validación local). Diferencias clave:
|
||||||
|
# - web sirve en https://testfuegos.comunes.org (tras el proxy assange), puerto host 8004
|
||||||
|
# - Mongo 7 AISLADO, sembrado con un volcado real de `fuegos` y contactos neutralizados
|
||||||
|
# (NUNCA apunta al rsmain de producción)
|
||||||
|
# - notificaciones en MODE=shadow / MATCHER_MODE=shadow: calcula y loguea, NO envía
|
||||||
|
# - mailhog captura TODO el correo (nada sale a un SMTP real)
|
||||||
|
# - node-red solo conversacional; si se prueba el bot, con un BOT DE PRUEBAS, nunca los reales
|
||||||
|
#
|
||||||
|
# Despliegue real: vía el role ansible roles/tcef_staging/ en el repo de Comunes
|
||||||
|
# (~/proyectos/sync/comunes/shared-l2/ansible/). Este compose es la fuente que ese role monta.
|
||||||
|
# Secretos: ./secrets/*.staging.* (gitignored). Uso: RUNBOOK.md.
|
||||||
|
|
||||||
|
name: tcef-staging
|
||||||
|
|
||||||
|
x-logging: &default-logging
|
||||||
|
driver: json-file
|
||||||
|
options:
|
||||||
|
max-size: "10m"
|
||||||
|
max-file: "3"
|
||||||
|
|
||||||
|
services:
|
||||||
|
mongo:
|
||||||
|
image: mongo:7
|
||||||
|
container_name: tcef-staging-mongo
|
||||||
|
command: ["--replSet", "rs0", "--bind_ip_all"]
|
||||||
|
volumes:
|
||||||
|
- mongo-data:/data/db
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "mongosh", "--quiet", "--eval", "db.adminCommand('ping').ok"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 12
|
||||||
|
start_period: 20s
|
||||||
|
restart: unless-stopped
|
||||||
|
logging: *default-logging
|
||||||
|
|
||||||
|
# One-shot: inicia el replica set de un nodo y marca las migraciones a v18
|
||||||
|
# (los datos reales llegan pre-migrados). Idéntico al compose de validación.
|
||||||
|
mongo-init:
|
||||||
|
image: mongo:7
|
||||||
|
depends_on:
|
||||||
|
mongo:
|
||||||
|
condition: service_healthy
|
||||||
|
restart: "no"
|
||||||
|
entrypoint:
|
||||||
|
- bash
|
||||||
|
- -ec
|
||||||
|
- |
|
||||||
|
mongosh --host mongo --quiet --eval 'try { rs.status().ok } catch (e) { rs.initiate({_id: "rs0", members: [{_id: 0, host: "mongo:27017"}]}) }'
|
||||||
|
for i in $$(seq 1 30); do
|
||||||
|
state=$$(mongosh --host mongo --quiet --eval 'try { rs.isMaster().ismaster } catch (e) { false }')
|
||||||
|
[ "$$state" = "true" ] && break
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
mongosh --host mongo --quiet fuegos --eval 'db.migrations.replaceOne({_id: "control"}, {_id: "control", version: 18, locked: false}, {upsert: true})'
|
||||||
|
echo "mongo-init: replica set + migrations OK"
|
||||||
|
logging: *default-logging
|
||||||
|
|
||||||
|
web:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
image: tcef-web:meteor3
|
||||||
|
container_name: tcef-staging-web
|
||||||
|
depends_on:
|
||||||
|
mongo-init:
|
||||||
|
condition: service_completed_successfully
|
||||||
|
environment:
|
||||||
|
PORT: "3000"
|
||||||
|
ROOT_URL: https://testfuegos.comunes.org
|
||||||
|
MONGO_URL: mongodb://mongo:27017/fuegos?replicaSet=rs0
|
||||||
|
METEOR_SETTINGS_FILE: /run/secrets/meteor-settings.json
|
||||||
|
volumes:
|
||||||
|
- ./secrets/meteor-settings.staging.json:/run/secrets/meteor-settings.json:ro
|
||||||
|
# Solo el proxy (assange) llega a este puerto; se publica en la red interna del host.
|
||||||
|
ports:
|
||||||
|
- "8004:3000"
|
||||||
|
restart: unless-stopped
|
||||||
|
logging: *default-logging
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
container_name: tcef-staging-redis
|
||||||
|
command: ["redis-server", "--appendonly", "yes"]
|
||||||
|
volumes:
|
||||||
|
- redis-data:/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "redis-cli", "ping"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 6
|
||||||
|
restart: unless-stopped
|
||||||
|
logging: *default-logging
|
||||||
|
|
||||||
|
notifications:
|
||||||
|
build:
|
||||||
|
context: ../tcef-notifications
|
||||||
|
image: tcef-notifications:local
|
||||||
|
container_name: tcef-staging-notifications
|
||||||
|
depends_on:
|
||||||
|
mongo-init:
|
||||||
|
condition: service_completed_successfully
|
||||||
|
redis:
|
||||||
|
condition: service_healthy
|
||||||
|
# secrets/notifications.staging.env → MODE=shadow, MATCHER_MODE=shadow, sin FCM,
|
||||||
|
# MAIL_URL=smtp://mailhog:1025. No envía nada a usuarios reales.
|
||||||
|
env_file:
|
||||||
|
- ./secrets/notifications.staging.env
|
||||||
|
restart: unless-stopped
|
||||||
|
logging: *default-logging
|
||||||
|
|
||||||
|
# Buzón SMTP de captura: TODO el correo (verificación/reset/notifs) aterriza aquí, nunca fuera.
|
||||||
|
# UI web en 127.0.0.1:8025 (acceder por túnel SSH). SMTP interno en mailhog:1025.
|
||||||
|
mailhog:
|
||||||
|
image: mailhog/mailhog:latest
|
||||||
|
container_name: tcef-staging-mailhog
|
||||||
|
environment:
|
||||||
|
MH_STORAGE: memory
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:8025:8025"
|
||||||
|
restart: unless-stopped
|
||||||
|
logging: *default-logging
|
||||||
|
|
||||||
|
node-red:
|
||||||
|
image: nodered/node-red:4.0
|
||||||
|
container_name: tcef-staging-node-red
|
||||||
|
volumes:
|
||||||
|
- nodered-data:/data
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:1880:1880"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "-qO", "/dev/null", "http://127.0.0.1:1880/"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
start_period: 30s
|
||||||
|
restart: unless-stopped
|
||||||
|
logging: *default-logging
|
||||||
|
|
||||||
|
# Importador NASA FIRMS → escribe activefires en ESTE mongo (no en prod), alimentando el
|
||||||
|
# matcher en shadow. No arranca con `up` (profile "importer"): lo dispara un systemd timer
|
||||||
|
# del host cada 15 min con `docker compose run --rm importer`.
|
||||||
|
# NOTA: requiere crear su Dockerfile en fires-csv-mongo-import/ (fase-3 §2). Hasta entonces,
|
||||||
|
# este servicio queda declarado pero su build context debe completarse.
|
||||||
|
importer:
|
||||||
|
profiles: ["importer"]
|
||||||
|
build:
|
||||||
|
context: ../todos-contra-el-fuego/fires-csv-mongo-import
|
||||||
|
image: tcef-nasa-importer:local
|
||||||
|
container_name: tcef-staging-importer
|
||||||
|
depends_on:
|
||||||
|
mongo-init:
|
||||||
|
condition: service_completed_successfully
|
||||||
|
environment:
|
||||||
|
MONGO_URL: mongodb://mongo:27017/fuegos?replicaSet=rs0
|
||||||
|
restart: "no"
|
||||||
|
logging: *default-logging
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
mongo-data:
|
||||||
|
redis-data:
|
||||||
|
nodered-data:
|
||||||
95
scripts/neutralize-contacts.mongo.js
Normal file
95
scripts/neutralize-contacts.mongo.js
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
// Neutraliza los datos de contacto en un volcado REAL de `fuegos` restaurado en el
|
||||||
|
// Mongo 7 de STAGING, para que ninguna notificación pueda alcanzar a un usuario real.
|
||||||
|
// IDEMPOTENTE: se puede ejecutar varias veces sin efectos acumulativos indeseados.
|
||||||
|
//
|
||||||
|
// Uso (desde el host de staging, contra el contenedor del compose):
|
||||||
|
// docker exec -i tcef-staging-mongo mongosh --quiet fuegos < scripts/neutralize-contacts.mongo.js
|
||||||
|
//
|
||||||
|
// SEGURIDAD: ejecutar SOLO contra el Mongo aislado de staging. Aborta si detecta que la
|
||||||
|
// conexión no es la esperada (heurística: nombre de host contiene 'shiva'/'simone'/'rbg'/'rosaparks').
|
||||||
|
|
||||||
|
(function guardAgainstProd() {
|
||||||
|
try {
|
||||||
|
const host = db.getMongo().host || '';
|
||||||
|
const prod = /shiva|simone|rbg|rosaparks|rsmain/i;
|
||||||
|
if (prod.test(host)) {
|
||||||
|
throw new Error('ABORTADO: la conexión (' + host + ') parece de PRODUCCIÓN. Este script solo debe correr contra el Mongo de staging.');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (/ABORTADO/.test(e.message)) { print(e.message); quit(1); }
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
const dbx = db.getSiblingDB('fuegos');
|
||||||
|
|
||||||
|
// 1) users.emails[].address -> "<_id>@invalid.test" (único por usuario) + verified:false
|
||||||
|
const rEmails = dbx.users.updateMany(
|
||||||
|
{ emails: { $type: 'array', $ne: [] } },
|
||||||
|
[{
|
||||||
|
$set: {
|
||||||
|
emails: {
|
||||||
|
$map: {
|
||||||
|
input: '$emails',
|
||||||
|
as: 'e',
|
||||||
|
in: {
|
||||||
|
address: { $concat: [{ $toString: '$_id' }, '@invalid.test'] },
|
||||||
|
verified: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
);
|
||||||
|
|
||||||
|
// 2) users: purgar tokens/handles de notificación (FCM y Telegram)
|
||||||
|
const rUserTokens = dbx.users.updateMany(
|
||||||
|
{}, { $unset: {
|
||||||
|
fireBaseToken: '',
|
||||||
|
telegramChatId: '',
|
||||||
|
telegramUsername: '',
|
||||||
|
telegramFirstName: '',
|
||||||
|
telegramLanguageCode: ''
|
||||||
|
}}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 3) users.services.google.email -> invalidar (evita PII en OAuth); conservar el resto de services
|
||||||
|
const rGoogle = dbx.users.updateMany(
|
||||||
|
{ 'services.google.email': { $exists: true } },
|
||||||
|
[{ $set: { 'services.google.email': { $concat: [{ $toString: '$_id' }, '@invalid.test'] } } }]
|
||||||
|
);
|
||||||
|
|
||||||
|
// 4) subscriptions: quitar el destino de Telegram (chatId) de las subs de ese tipo
|
||||||
|
const rSubs = dbx.subscriptions.updateMany(
|
||||||
|
{ chatId: { $exists: true } }, { $unset: { chatId: '' } }
|
||||||
|
);
|
||||||
|
|
||||||
|
// 5) Cola de Mail-Time: el volcado de prod trae correos ya encolados con el
|
||||||
|
// DESTINATARIO real "horneado" (p.ej. de 2020). Neutralizar users.emails NO los
|
||||||
|
// toca; la web (isMailServer) los drenaría. Se vacían las colas de la cola/jobs.
|
||||||
|
// (En staging MAIL_URL apunta a Mailhog, así que aunque quedaran, se capturarían;
|
||||||
|
// esto es defensa en profundidad para no arrastrar direcciones reales.)
|
||||||
|
const rMailQueue = dbx.getCollection('__mailTimeQueue__').deleteMany({});
|
||||||
|
const rMailJobs = dbx.getCollection('__JobTasks__mailTimeQueue').deleteMany({});
|
||||||
|
|
||||||
|
printjson({
|
||||||
|
usersEmailsNeutralizados: rEmails.modifiedCount,
|
||||||
|
usersTokensPurgados: rUserTokens.modifiedCount,
|
||||||
|
googleEmailsInvalidados: rGoogle.modifiedCount,
|
||||||
|
subsChatIdEliminados: rSubs.modifiedCount,
|
||||||
|
colaCorreoBorrada: rMailQueue.deletedCount,
|
||||||
|
colaJobsBorrada: rMailJobs.deletedCount
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verificación rápida (debe dar 0 en todas). OJO: filtrar por $exists ANTES del
|
||||||
|
// $not/regex — si no, los usuarios SIN email cuentan como "no invalidados" (falso positivo).
|
||||||
|
const conToken = dbx.users.countDocuments({ fireBaseToken: { $exists: true } });
|
||||||
|
const emailMalo = dbx.users.countDocuments({ 'emails.address': { $exists: true, $not: /@invalid\.test$/ } });
|
||||||
|
const googleMalo = dbx.users.countDocuments({ 'services.google.email': { $exists: true, $not: /@invalid\.test$/ } });
|
||||||
|
const subsConChat = dbx.subscriptions.countDocuments({ chatId: { $exists: true } });
|
||||||
|
const colaCorreo = dbx.getCollection('__mailTimeQueue__').countDocuments({});
|
||||||
|
printjson({ verificacion: { usersConFireBaseToken: conToken, usersConEmailRealVisible: emailMalo, usersConGoogleEmailReal: googleMalo, subsConChatId: subsConChat, correosEnCola: colaCorreo } });
|
||||||
|
if (conToken === 0 && emailMalo === 0 && googleMalo === 0 && subsConChat === 0 && colaCorreo === 0) {
|
||||||
|
print('OK: contactos neutralizados. Ninguna notificación puede alcanzar a un usuario real.');
|
||||||
|
} else {
|
||||||
|
print('AVISO: quedan campos sin neutralizar — revisar antes de exponer el staging.');
|
||||||
|
}
|
||||||
46
secrets/meteor-settings.staging.json.example
Normal file
46
secrets/meteor-settings.staging.json.example
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
{
|
||||||
|
"// AVISO": "Plantilla de STAGING (testfuegos.comunes.org). Copia a meteor-settings.staging.json y rellena los valores reales desde tcef-private-config/local/todos-contra-el-fuego-web/settings-staging.json. El fichero real está gitignorado.",
|
||||||
|
"PrerenderIO": {
|
||||||
|
"serviceUrl": "http://groucho.comunes.org:3000/render",
|
||||||
|
"token": "PRERENDER_TOKEN"
|
||||||
|
},
|
||||||
|
"gmaps": {
|
||||||
|
"key": "GMAPS_BROWSER_KEY",
|
||||||
|
"serverKey": "GMAPS_SERVER_KEY"
|
||||||
|
},
|
||||||
|
"piwik": {
|
||||||
|
"url": "https://piwik.comunes.org/piwik.php",
|
||||||
|
"site_id": 7
|
||||||
|
},
|
||||||
|
"sentryPrivateDSN": "",
|
||||||
|
"public": {
|
||||||
|
"sentryPublicDSN": ""
|
||||||
|
},
|
||||||
|
"private": {
|
||||||
|
"testEmail": "vjrj@ourproject.org",
|
||||||
|
"// mail": "STAGING: correo capturado por Mailhog del compose. testMailer=false para no emitir 4 correos de autoprueba al arrancar; debugMailer=true para loguear la cola.",
|
||||||
|
"testMailer": false,
|
||||||
|
"debugMailer": true,
|
||||||
|
"MAIL_URL": "smtp://mailhog:1025",
|
||||||
|
"isMailServer": true,
|
||||||
|
"fireIconUrl": "https://testfuegos.comunes.org/n-fire-marker.png",
|
||||||
|
"OAuth": {
|
||||||
|
"google": {
|
||||||
|
"clientId": "GOOGLE_OAUTH_CLIENT_ID",
|
||||||
|
"secret": "GOOGLE_OAUTH_SECRET",
|
||||||
|
"loginStyle": "popup"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"// fcm": "STAGING: sin fcmApiToken. El push lo gestiona tcef-notifications (en shadow, sin service account).",
|
||||||
|
"proxies_count": 1,
|
||||||
|
"ironPassword": "IRON_PASSWORD",
|
||||||
|
"internalApiToken": "INTERNAL_API_TOKEN",
|
||||||
|
"cron": {
|
||||||
|
"debug": false
|
||||||
|
},
|
||||||
|
"twitter": {
|
||||||
|
"es": { "enabled": false },
|
||||||
|
"en": { "enabled": false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
40
secrets/notifications.staging.env.example
Normal file
40
secrets/notifications.staging.env.example
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
# tcef-notifications en STAGING (testfuegos.comunes.org). Copia a notifications.staging.env.
|
||||||
|
# Referencia completa: ../../tcef-notifications/.env.example
|
||||||
|
#
|
||||||
|
# CERO ENVÍOS A USUARIOS REALES:
|
||||||
|
# MODE=shadow → calcula decisiones y las loguea, NO envía (ver src/mode.ts decideSend)
|
||||||
|
# MATCHER_MODE=shadow→ el matcher computa candidatos a notificar, NO inserta/envía
|
||||||
|
# sin FCM_SERVICE_ACCOUNT → no se construye el cliente FCM (imposible enviar push)
|
||||||
|
# MAIL_URL=smtp://mailhog:1025 → cualquier correo se captura en Mailhog, nunca sale
|
||||||
|
# telegram FUERA de OWNED_CHANNELS → los bots reales ES/EN ni se tocan
|
||||||
|
|
||||||
|
MODE=shadow
|
||||||
|
KILL_SWITCH=0
|
||||||
|
OWNED_CHANNELS=fcm,email
|
||||||
|
MATCHER_MODE=shadow
|
||||||
|
LOG_LEVEL=debug
|
||||||
|
|
||||||
|
# Infra del compose de staging (nombres de servicio de docker-compose.staging.yml)
|
||||||
|
MONGO_URL=mongodb://mongo:27017/fuegos?replicaSet=rs0
|
||||||
|
MONGO_DB=fuegos
|
||||||
|
REDIS_HOST=redis
|
||||||
|
REDIS_PORT=6379
|
||||||
|
|
||||||
|
POLL_INTERVAL_MS=10000
|
||||||
|
POLL_BATCH_SIZE=500
|
||||||
|
|
||||||
|
# Captura de correo (no un SMTP real). En shadow no se envía, pero se deja apuntando a Mailhog
|
||||||
|
# como red de seguridad si alguien sube el MODE por error.
|
||||||
|
MAIL_URL=smtp://mailhog:1025
|
||||||
|
MAIL_FROM=Todos contra el Fuego (STAGING) <no-reply@testfuegos.comunes.org>
|
||||||
|
|
||||||
|
# NO configurar en staging (dejar comentado): sin service account no hay push posible.
|
||||||
|
# FCM_SERVICE_ACCOUNT=/run/secrets/firebase-service-account.json
|
||||||
|
# FCM_PROJECT_ID=angular-cosmos-108908
|
||||||
|
|
||||||
|
# OBLIGATORIO si MATCHER_MODE != off: el matcher lo exige para sellar notification.sealed
|
||||||
|
# (createMatchContext falla al arrancar sin él, aun en shadow). = ironPassword de los settings.
|
||||||
|
IRON_PASSWORD=IRON_PASSWORD_DE_LOS_SETTINGS
|
||||||
|
|
||||||
|
ROOT_URL=https://testfuegos.comunes.org/
|
||||||
|
SERVICE_NAME=tcef-notifications-staging
|
||||||
Loading…
Add table
Add a link
Reference in a new issue