mirror of
https://origin.cursor.com/mrdevmx/panels.git
synced 2026-10-09 20:43:17 +00:00
ops: generar accesos por ambiente y verificar Postgres, Redis y Contabo
Scripts create-accesses / verify-connectivity para un Postgres+Redis+bucket por ambiente. /v1/health y el arranque de la API incluyen sonda de storage. Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
This commit is contained in:
parent
d36c287a82
commit
1ac04996b6
8 changed files with 351 additions and 12 deletions
12
.github/workflows/ci.yml
vendored
12
.github/workflows/ci.yml
vendored
|
|
@ -83,6 +83,18 @@ jobs:
|
||||||
REDIS_URL_CORE: redis://panels_core_redis:ci-core-redis@localhost:6379
|
REDIS_URL_CORE: redis://panels_core_redis:ci-core-redis@localhost:6379
|
||||||
run: ./db/provision/verify-isolation.sh
|
run: ./db/provision/verify-isolation.sh
|
||||||
|
|
||||||
|
- name: Verificar conectividad Postgres/Redis (S3 opcional en CI)
|
||||||
|
env:
|
||||||
|
DATABASE_URL_PLATFORM: postgresql://panels_platform_app:ci-platform-app@localhost:5432/panels_platform
|
||||||
|
DATABASE_URL_PLATFORM_OWNER: postgresql://panels_platform_owner:ci-platform-owner@localhost:5432/panels_platform
|
||||||
|
DATABASE_URL_IAM: postgresql://panels_iam_app:ci-iam-app@localhost:5432/panels_product
|
||||||
|
DATABASE_URL_IAM_OWNER: postgresql://panels_iam_owner:ci-iam-owner@localhost:5432/panels_product
|
||||||
|
DATABASE_URL_CORE: postgresql://panels_core_app:ci-core-app@localhost:5432/panels_product
|
||||||
|
DATABASE_URL_CORE_OWNER: postgresql://panels_core_owner:ci-core-owner@localhost:5432/panels_product
|
||||||
|
REDIS_URL_IAM: redis://panels_iam_redis:ci-iam-redis@localhost:6379
|
||||||
|
REDIS_URL_CORE: redis://panels_core_redis:ci-core-redis@localhost:6379
|
||||||
|
run: ./db/provision/verify-connectivity.sh
|
||||||
|
|
||||||
- name: deno check
|
- name: deno check
|
||||||
working-directory: api
|
working-directory: api
|
||||||
run: deno check main.ts
|
run: deno check main.ts
|
||||||
|
|
|
||||||
16
api/main.ts
16
api/main.ts
|
|
@ -61,7 +61,7 @@ import {
|
||||||
lineAmount,
|
lineAmount,
|
||||||
listBudget,
|
listBudget,
|
||||||
} from "./budget.ts";
|
} from "./budget.ts";
|
||||||
import { companyDocKey, getObject, projectDocKey, workerDocKey } from "./storage.ts";
|
import { companyDocKey, getObject, pingStorage, projectDocKey, workerDocKey } from "./storage.ts";
|
||||||
import { cacheCore, cacheKeyCore } from "./cache.ts";
|
import { cacheCore, cacheKeyCore } from "./cache.ts";
|
||||||
import { pingRedis } from "./redis.ts";
|
import { pingRedis } from "./redis.ts";
|
||||||
|
|
||||||
|
|
@ -105,13 +105,15 @@ app.use(
|
||||||
// Postgres/Redis, /v1/health de verdad toca las tres conexiones Postgres y
|
// Postgres/Redis, /v1/health de verdad toca las tres conexiones Postgres y
|
||||||
// las dos de Redis, no solo responde estático.
|
// las dos de Redis, no solo responde estático.
|
||||||
app.get("/v1/health", async (c) => {
|
app.get("/v1/health", async (c) => {
|
||||||
const [core, platform, redis] = await Promise.all([
|
const [core, platform, redis, storage] = await Promise.all([
|
||||||
pingCoreDb().then(() => true).catch(() => false),
|
pingCoreDb().then(() => true).catch(() => false),
|
||||||
pingPlatformDb().then(() => true).catch(() => false),
|
pingPlatformDb().then(() => true).catch(() => false),
|
||||||
pingRedis(),
|
pingRedis(),
|
||||||
|
pingStorage(),
|
||||||
]);
|
]);
|
||||||
const ok = core && platform && redis.iam && redis.core;
|
const storageOk = storage.ok && (storage.backend === "local" || storage.configured);
|
||||||
return c.json({ ok, core, platform, redis }, ok ? 200 : 503);
|
const ok = core && platform && redis.iam && redis.core && storageOk;
|
||||||
|
return c.json({ ok, core, platform, redis, storage }, ok ? 200 : 503);
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post("/v1/auth/login", async (c) => {
|
app.post("/v1/auth/login", async (c) => {
|
||||||
|
|
@ -1260,6 +1262,12 @@ await Promise.all([
|
||||||
pingRedis().then((r) => {
|
pingRedis().then((r) => {
|
||||||
if (!r.iam || !r.core) throw new Error("Redis (iam/core) no responde");
|
if (!r.iam || !r.core) throw new Error("Redis (iam/core) no responde");
|
||||||
}),
|
}),
|
||||||
|
pingStorage().then((s) => {
|
||||||
|
if (!s.ok) throw new Error("Storage (Contabo o disco local) no responde");
|
||||||
|
if (!config.isDev && !s.configured) {
|
||||||
|
throw new Error("S3_ENDPOINT/S3_BUCKET/S3_* son obligatorios fuera de desarrollo");
|
||||||
|
}
|
||||||
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
Deno.serve({ port, hostname: "0.0.0.0" }, app.fetch);
|
Deno.serve({ port, hostname: "0.0.0.0" }, app.fetch);
|
||||||
|
|
|
||||||
35
api/scripts/verify-storage.ts
Normal file
35
api/scripts/verify-storage.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
/**
|
||||||
|
* Sonda de Contabo (S3): HeadBucket + put/get/delete.
|
||||||
|
*
|
||||||
|
* Desde api/:
|
||||||
|
* deno run --allow-net --allow-env --allow-read --allow-write scripts/verify-storage.ts
|
||||||
|
*
|
||||||
|
* Requiere S3_ENDPOINT, S3_BUCKET, S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY.
|
||||||
|
* Si REQUIRE_S3=1 y no hay credenciales, sale con error (staging/prod).
|
||||||
|
* Si no están, sale 0 y avisa (dev local con disco).
|
||||||
|
*/
|
||||||
|
import { config } from "../config.ts";
|
||||||
|
import { pingStorage, probeStorageReadWrite, s3Configured } from "../storage.ts";
|
||||||
|
|
||||||
|
const requireS3 = ["1", "true", "yes"].includes((Deno.env.get("REQUIRE_S3") ?? "").toLowerCase());
|
||||||
|
|
||||||
|
if (!s3Configured()) {
|
||||||
|
const msg =
|
||||||
|
"S3 no configurado (S3_ENDPOINT / S3_BUCKET / S3_ACCESS_KEY_ID / S3_SECRET_ACCESS_KEY).";
|
||||||
|
if (requireS3) {
|
||||||
|
console.error(`FAIL - ${msg} Obligatorio en staging/producción.`);
|
||||||
|
Deno.exit(1);
|
||||||
|
}
|
||||||
|
console.log(`SKIP - ${msg} En este ambiente se usará disco local.`);
|
||||||
|
Deno.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`S3 endpoint=${config.s3Endpoint} bucket=${config.s3Bucket} region=${config.s3Region}`);
|
||||||
|
const ping = await pingStorage();
|
||||||
|
if (!ping.ok) {
|
||||||
|
console.error("FAIL - HeadBucket: no se pudo alcanzar el bucket (credenciales, red o nombre).");
|
||||||
|
Deno.exit(1);
|
||||||
|
}
|
||||||
|
console.log("OK - HeadBucket");
|
||||||
|
await probeStorageReadWrite();
|
||||||
|
console.log("OK - put/get/delete de objeto sonda");
|
||||||
|
|
@ -1,4 +1,10 @@
|
||||||
import { S3Client, PutObjectCommand, GetObjectCommand } from "npm:@aws-sdk/client-s3@3";
|
import {
|
||||||
|
S3Client,
|
||||||
|
PutObjectCommand,
|
||||||
|
GetObjectCommand,
|
||||||
|
HeadBucketCommand,
|
||||||
|
DeleteObjectCommand,
|
||||||
|
} from "npm:@aws-sdk/client-s3@3";
|
||||||
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
||||||
import { dirname, join } from "node:path";
|
import { dirname, join } from "node:path";
|
||||||
import { config, DATA_DIR } from "./config.ts";
|
import { config, DATA_DIR } from "./config.ts";
|
||||||
|
|
@ -20,10 +26,48 @@ import { config, DATA_DIR } from "./config.ts";
|
||||||
|
|
||||||
let client: S3Client | null = null;
|
let client: S3Client | null = null;
|
||||||
|
|
||||||
function s3Configured(): boolean {
|
export function s3Configured(): boolean {
|
||||||
return !!(config.s3Endpoint && config.s3Bucket && config.s3AccessKeyId && config.s3SecretAccessKey);
|
return !!(config.s3Endpoint && config.s3Bucket && config.s3AccessKeyId && config.s3SecretAccessKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type StoragePing = {
|
||||||
|
configured: boolean;
|
||||||
|
ok: boolean;
|
||||||
|
backend: "s3" | "local";
|
||||||
|
};
|
||||||
|
|
||||||
|
/** HeadBucket (S3) o escritura de prueba en disco local. */
|
||||||
|
export async function pingStorage(): Promise<StoragePing> {
|
||||||
|
if (!s3Configured()) {
|
||||||
|
try {
|
||||||
|
await mkdir(join(DATA_DIR, "local-objects"), { recursive: true });
|
||||||
|
return { configured: false, ok: true, backend: "local" };
|
||||||
|
} catch {
|
||||||
|
return { configured: false, ok: false, backend: "local" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await getClient().send(new HeadBucketCommand({ Bucket: config.s3Bucket }));
|
||||||
|
return { configured: true, ok: true, backend: "s3" };
|
||||||
|
} catch {
|
||||||
|
return { configured: true, ok: false, backend: "s3" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Put + get + delete de un objeto sonda. No dejar basura en el bucket. */
|
||||||
|
export async function probeStorageReadWrite(): Promise<void> {
|
||||||
|
const key = `_panels/connectivity-probe-${crypto.randomUUID()}`;
|
||||||
|
const payload = new TextEncoder().encode("panels-connectivity-probe");
|
||||||
|
await putObject(key, payload);
|
||||||
|
const got = await getObject(key);
|
||||||
|
if (new TextDecoder().decode(got) !== "panels-connectivity-probe") {
|
||||||
|
throw new Error("El objeto sonda no coincide con lo escrito");
|
||||||
|
}
|
||||||
|
if (s3Configured()) {
|
||||||
|
await getClient().send(new DeleteObjectCommand({ Bucket: config.s3Bucket, Key: key }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function getClient(): S3Client {
|
function getClient(): S3Client {
|
||||||
if (!client) {
|
if (!client) {
|
||||||
client = new S3Client({
|
client = new S3Client({
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,35 @@ El script verifica automáticamente que cruzar de módulo devuelva `NOPERM`
|
||||||
(ver "Riesgos y mitigaciones" del plan: un prefijo de llave sin ACL detrás
|
(ver "Riesgos y mitigaciones" del plan: un prefijo de llave sin ACL detrás
|
||||||
no aísla nada).
|
no aísla nada).
|
||||||
|
|
||||||
|
## Accesos de un ambiente (Coolify + Contabo)
|
||||||
|
|
||||||
|
**1 Postgres + 1 Redis + 1 bucket por ambiente.** El servidor lo crea
|
||||||
|
Coolify/Contabo; este repo solo genera roles y valida que contesten.
|
||||||
|
|
||||||
|
1. En Coolify: recurso Postgres y recurso Redis de **ese** ambiente.
|
||||||
|
En Contabo: bucket (ej. `panels-prod` / `panels-staging`) + access key.
|
||||||
|
2. Generar secretos y crear roles/ACLs contra esos hosts:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export PGHOST=... PGUSER=postgres PGPASSWORD=... # admin que da Coolify
|
||||||
|
export REDIS_ADMIN_URL="redis://:...@host:6379"
|
||||||
|
export S3_ENDPOINT=https://usc1.contabostorage.com
|
||||||
|
export S3_BUCKET=panels-prod
|
||||||
|
export S3_ACCESS_KEY_ID=... S3_SECRET_ACCESS_KEY=...
|
||||||
|
./db/provision/create-accesses.sh --apply --verify --out .env.prod.local
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Pegar el contenido de `.env.prod.local` (gitignored) en las env del `api`.
|
||||||
|
4. Sin `--apply`, el script solo imprime el bloque; no toca servidores.
|
||||||
|
|
||||||
|
Comprobar conectividad después, sin reprovisionar:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
set -a && source .env.prod.local && set +a
|
||||||
|
REQUIRE_S3=1 ./db/provision/verify-connectivity.sh # staging/prod
|
||||||
|
./db/provision/verify-isolation.sh
|
||||||
|
```
|
||||||
|
|
||||||
## Qué falta hacer manualmente en Coolify (staging/producción)
|
## Qué falta hacer manualmente en Coolify (staging/producción)
|
||||||
|
|
||||||
Estos scripts asumen que ya existe un servidor Postgres y un servidor Redis
|
Estos scripts asumen que ya existe un servidor Postgres y un servidor Redis
|
||||||
|
|
@ -68,9 +97,10 @@ tiene acceso a la cuenta de Coolify del usuario, así que:
|
||||||
|
|
||||||
1. Crear el recurso Postgres gestionado en Coolify para el ambiente.
|
1. Crear el recurso Postgres gestionado en Coolify para el ambiente.
|
||||||
2. Crear el recurso Redis gestionado en Coolify para el ambiente.
|
2. Crear el recurso Redis gestionado en Coolify para el ambiente.
|
||||||
3. Correr estos scripts contra ambos usando las credenciales admin que da Coolify.
|
3. Crear el bucket Contabo de ese ambiente.
|
||||||
4. Cargar los secrets resultantes (`DATABASE_URL_*`, `REDIS_URL_*`) en la
|
4. Correr `create-accesses.sh --apply --verify` (o los SQL 01-04 + `05-redis-acl.sh`).
|
||||||
configuración del servicio `api` de ese ambiente.
|
5. Cargar los secrets resultantes (`DATABASE_URL_*`, `REDIS_URL_*`, `S3_*`)
|
||||||
|
en la configuración del servicio `api` de ese ambiente.
|
||||||
|
|
||||||
## Desarrollo local
|
## Desarrollo local
|
||||||
|
|
||||||
|
|
|
||||||
131
db/provision/create-accesses.sh
Executable file
131
db/provision/create-accesses.sh
Executable file
|
|
@ -0,0 +1,131 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# PANELS · genera secretos de UN ambiente y, si hay superusuario, crea
|
||||||
|
# roles/bases/ACLs. No crea el servidor Postgres, Redis ni el bucket:
|
||||||
|
# esos se levantan en Coolify / Contabo; este script solo deja los accesos.
|
||||||
|
#
|
||||||
|
# Uso (solo imprimir bloque .env, sin tocar servidores):
|
||||||
|
# PGHOST=pg.interno REDIS_HOST=redis.interno S3_BUCKET=panels-prod \
|
||||||
|
# ./db/provision/create-accesses.sh
|
||||||
|
#
|
||||||
|
# Uso (crear roles en un Postgres/Redis ya levantados):
|
||||||
|
# PGHOST=... PGUSER=postgres PGPASSWORD=... REDIS_ADMIN_URL=redis://:...@host:6379 \
|
||||||
|
# ./db/provision/create-accesses.sh --apply --verify
|
||||||
|
#
|
||||||
|
# Flags:
|
||||||
|
# --apply corre 01-04 SQL + 05 Redis ACL con las claves generadas
|
||||||
|
# --verify corre verify-connectivity.sh al final (implica tener URLs)
|
||||||
|
# --out FILE escribe el bloque .env (FILE debe estar gitignored, ej. .env.prod.local)
|
||||||
|
set -euo pipefail
|
||||||
|
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
APPLY=0
|
||||||
|
VERIFY=0
|
||||||
|
OUT=""
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--apply) APPLY=1; shift ;;
|
||||||
|
--verify) VERIFY=1; shift ;;
|
||||||
|
--out) OUT="$2"; shift 2 ;;
|
||||||
|
-h|--help) sed -n '2,20p' "$0"; exit 0 ;;
|
||||||
|
*) echo "Flag desconocido: $1" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
rand24() { openssl rand -hex 24; }
|
||||||
|
rand32() { openssl rand -hex 32; }
|
||||||
|
|
||||||
|
PGHOST="${PGHOST:-127.0.0.1}"
|
||||||
|
PGPORT="${PGPORT:-5432}"
|
||||||
|
REDIS_HOST="${REDIS_HOST:-127.0.0.1}"
|
||||||
|
REDIS_PORT="${REDIS_PORT:-6379}"
|
||||||
|
PGUSER="${PGUSER:-postgres}"
|
||||||
|
|
||||||
|
PLATFORM_OWNER_PASSWORD="${PLATFORM_OWNER_PASSWORD:-$(rand24)}"
|
||||||
|
PLATFORM_APP_PASSWORD="${PLATFORM_APP_PASSWORD:-$(rand24)}"
|
||||||
|
IAM_OWNER_PASSWORD="${IAM_OWNER_PASSWORD:-$(rand24)}"
|
||||||
|
IAM_APP_PASSWORD="${IAM_APP_PASSWORD:-$(rand24)}"
|
||||||
|
CORE_OWNER_PASSWORD="${CORE_OWNER_PASSWORD:-$(rand24)}"
|
||||||
|
CORE_APP_PASSWORD="${CORE_APP_PASSWORD:-$(rand24)}"
|
||||||
|
IAM_REDIS_PASSWORD="${IAM_REDIS_PASSWORD:-$(rand24)}"
|
||||||
|
CORE_REDIS_PASSWORD="${CORE_REDIS_PASSWORD:-$(rand24)}"
|
||||||
|
SESSION_SECRET="${SESSION_SECRET:-$(rand32)}"
|
||||||
|
DOCS_KEY="${DOCS_KEY:-$(rand32)}"
|
||||||
|
|
||||||
|
block() {
|
||||||
|
cat <<EOF
|
||||||
|
# PANELS · secretos de un solo ambiente (no mezclar prod/staging/dev)
|
||||||
|
SESSION_SECRET=${SESSION_SECRET}
|
||||||
|
DOCS_KEY=${DOCS_KEY}
|
||||||
|
|
||||||
|
DATABASE_URL_PLATFORM=postgresql://panels_platform_app:${PLATFORM_APP_PASSWORD}@${PGHOST}:${PGPORT}/panels_platform
|
||||||
|
DATABASE_URL_PLATFORM_OWNER=postgresql://panels_platform_owner:${PLATFORM_OWNER_PASSWORD}@${PGHOST}:${PGPORT}/panels_platform
|
||||||
|
DATABASE_URL_IAM=postgresql://panels_iam_app:${IAM_APP_PASSWORD}@${PGHOST}:${PGPORT}/panels_product
|
||||||
|
DATABASE_URL_IAM_OWNER=postgresql://panels_iam_owner:${IAM_OWNER_PASSWORD}@${PGHOST}:${PGPORT}/panels_product
|
||||||
|
DATABASE_URL_CORE=postgresql://panels_core_app:${CORE_APP_PASSWORD}@${PGHOST}:${PGPORT}/panels_product
|
||||||
|
DATABASE_URL_CORE_OWNER=postgresql://panels_core_owner:${CORE_OWNER_PASSWORD}@${PGHOST}:${PGPORT}/panels_product
|
||||||
|
|
||||||
|
REDIS_URL_IAM=redis://panels_iam_redis:${IAM_REDIS_PASSWORD}@${REDIS_HOST}:${REDIS_PORT}
|
||||||
|
REDIS_URL_CORE=redis://panels_core_redis:${CORE_REDIS_PASSWORD}@${REDIS_HOST}:${REDIS_PORT}
|
||||||
|
|
||||||
|
# Contabo -- rellenar endpoint/keys del Object Storage de ESTE ambiente
|
||||||
|
S3_ENDPOINT=${S3_ENDPOINT:-}
|
||||||
|
S3_BUCKET=${S3_BUCKET:-}
|
||||||
|
S3_REGION=${S3_REGION:-usc1}
|
||||||
|
S3_ACCESS_KEY_ID=${S3_ACCESS_KEY_ID:-}
|
||||||
|
S3_SECRET_ACCESS_KEY=${S3_SECRET_ACCESS_KEY:-}
|
||||||
|
|
||||||
|
# Passwords sueltos (provision / compose local)
|
||||||
|
POSTGRES_SUPERUSER_PASSWORD=${PGPASSWORD:-}
|
||||||
|
PLATFORM_OWNER_PASSWORD=${PLATFORM_OWNER_PASSWORD}
|
||||||
|
PLATFORM_APP_PASSWORD=${PLATFORM_APP_PASSWORD}
|
||||||
|
IAM_OWNER_PASSWORD=${IAM_OWNER_PASSWORD}
|
||||||
|
IAM_APP_PASSWORD=${IAM_APP_PASSWORD}
|
||||||
|
CORE_OWNER_PASSWORD=${CORE_OWNER_PASSWORD}
|
||||||
|
CORE_APP_PASSWORD=${CORE_APP_PASSWORD}
|
||||||
|
IAM_REDIS_PASSWORD=${IAM_REDIS_PASSWORD}
|
||||||
|
CORE_REDIS_PASSWORD=${CORE_REDIS_PASSWORD}
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ "$APPLY" == "1" ]]; then
|
||||||
|
: "${PGPASSWORD:?PGPASSWORD del superusuario es obligatorio con --apply}"
|
||||||
|
SUPERUSER_URL="postgresql://${PGUSER}:${PGPASSWORD}@${PGHOST}:${PGPORT}/postgres"
|
||||||
|
echo "== Aplicando roles/bases/esquemas en ${PGHOST}:${PGPORT} =="
|
||||||
|
psql "$SUPERUSER_URL" \
|
||||||
|
-v platform_owner_pw="$PLATFORM_OWNER_PASSWORD" -v platform_app_pw="$PLATFORM_APP_PASSWORD" \
|
||||||
|
-v iam_owner_pw="$IAM_OWNER_PASSWORD" -v iam_app_pw="$IAM_APP_PASSWORD" \
|
||||||
|
-v core_owner_pw="$CORE_OWNER_PASSWORD" -v core_app_pw="$CORE_APP_PASSWORD" \
|
||||||
|
-f "$HERE/01-roles.sql"
|
||||||
|
psql "$SUPERUSER_URL" -f "$HERE/02-databases.sql"
|
||||||
|
psql "postgresql://${PGUSER}:${PGPASSWORD}@${PGHOST}:${PGPORT}/panels_platform" -f "$HERE/03-platform-database.sql"
|
||||||
|
psql "postgresql://${PGUSER}:${PGPASSWORD}@${PGHOST}:${PGPORT}/panels_product" -f "$HERE/04-product-database.sql"
|
||||||
|
if [[ -n "${REDIS_ADMIN_URL:-}" ]]; then
|
||||||
|
echo "== Aplicando ACLs Redis =="
|
||||||
|
REDIS_ADMIN_URL="$REDIS_ADMIN_URL" \
|
||||||
|
IAM_REDIS_PASSWORD="$IAM_REDIS_PASSWORD" \
|
||||||
|
CORE_REDIS_PASSWORD="$CORE_REDIS_PASSWORD" \
|
||||||
|
"$HERE/05-redis-acl.sh"
|
||||||
|
else
|
||||||
|
echo "SKIP - REDIS_ADMIN_URL no definido; no se crearon usuarios ACL"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
ENV_TEXT="$(block)"
|
||||||
|
echo "$ENV_TEXT"
|
||||||
|
if [[ -n "$OUT" ]]; then
|
||||||
|
umask 077
|
||||||
|
printf '%s\n' "$ENV_TEXT" > "$OUT"
|
||||||
|
echo "Escrito $OUT (permisos 600). No lo subas a git." >&2
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$VERIFY" == "1" ]]; then
|
||||||
|
echo "== Verificando conectividad ==" >&2
|
||||||
|
tmp="$(mktemp)"
|
||||||
|
umask 077
|
||||||
|
printf '%s\n' "$ENV_TEXT" > "$tmp"
|
||||||
|
set -a
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
source "$tmp"
|
||||||
|
set +a
|
||||||
|
rm -f "$tmp"
|
||||||
|
"$HERE/verify-connectivity.sh"
|
||||||
|
fi
|
||||||
79
db/provision/verify-connectivity.sh
Executable file
79
db/provision/verify-connectivity.sh
Executable file
|
|
@ -0,0 +1,79 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# PANELS · comprueba que ESTE ambiente responde: Postgres (6 roles),
|
||||||
|
# Redis (2 ACL) y Contabo (opcional; obligatorio si REQUIRE_S3=1).
|
||||||
|
#
|
||||||
|
# No crea recursos. Carga URLs desde el entorno (source .env.dev-local o
|
||||||
|
# las vars de Coolify).
|
||||||
|
#
|
||||||
|
# Uso:
|
||||||
|
# set -a && source .env.dev-local && set +a
|
||||||
|
# ./db/provision/verify-connectivity.sh
|
||||||
|
# REQUIRE_S3=1 ./db/provision/verify-connectivity.sh # staging/prod
|
||||||
|
set -uo pipefail
|
||||||
|
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||||
|
fail=0
|
||||||
|
check() {
|
||||||
|
local desc="$1"; shift
|
||||||
|
if "$@" >/tmp/panels-conn-out.$$ 2>&1; then
|
||||||
|
echo "OK - $desc"
|
||||||
|
else
|
||||||
|
echo "FAIL - $desc"
|
||||||
|
cat /tmp/panels-conn-out.$$
|
||||||
|
fail=1
|
||||||
|
fi
|
||||||
|
rm -f /tmp/panels-conn-out.$$
|
||||||
|
}
|
||||||
|
|
||||||
|
need() {
|
||||||
|
local name="$1"
|
||||||
|
if [[ -z "${!name:-}" ]]; then
|
||||||
|
echo "FAIL - falta $name"
|
||||||
|
fail=1
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "== Postgres =="
|
||||||
|
need DATABASE_URL_PLATFORM && check "platform_app SELECT 1" psql "$DATABASE_URL_PLATFORM" -c "SELECT 1;"
|
||||||
|
need DATABASE_URL_IAM && check "iam_app SELECT 1" psql "$DATABASE_URL_IAM" -c "SELECT 1;"
|
||||||
|
need DATABASE_URL_CORE && check "core_app SELECT 1" psql "$DATABASE_URL_CORE" -c "SELECT 1;"
|
||||||
|
if [[ -n "${DATABASE_URL_PLATFORM_OWNER:-}" ]]; then
|
||||||
|
check "platform_owner SELECT 1" psql "$DATABASE_URL_PLATFORM_OWNER" -c "SELECT 1;"
|
||||||
|
else
|
||||||
|
echo "SKIP - DATABASE_URL_PLATFORM_OWNER (solo hace falta para Liquibase)"
|
||||||
|
fi
|
||||||
|
if [[ -n "${DATABASE_URL_IAM_OWNER:-}" ]]; then
|
||||||
|
check "iam_owner SELECT 1" psql "$DATABASE_URL_IAM_OWNER" -c "SELECT 1;"
|
||||||
|
else
|
||||||
|
echo "SKIP - DATABASE_URL_IAM_OWNER"
|
||||||
|
fi
|
||||||
|
if [[ -n "${DATABASE_URL_CORE_OWNER:-}" ]]; then
|
||||||
|
check "core_owner SELECT 1" psql "$DATABASE_URL_CORE_OWNER" -c "SELECT 1;"
|
||||||
|
else
|
||||||
|
echo "SKIP - DATABASE_URL_CORE_OWNER"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "== Redis =="
|
||||||
|
need REDIS_URL_IAM && check "iam redis PING" redis-cli -u "$REDIS_URL_IAM" PING
|
||||||
|
need REDIS_URL_CORE && check "core redis PING" redis-cli -u "$REDIS_URL_CORE" PING
|
||||||
|
|
||||||
|
echo "== Contabo (S3) =="
|
||||||
|
if command -v deno >/dev/null 2>&1; then
|
||||||
|
if (cd "$ROOT/api" && REQUIRE_S3="${REQUIRE_S3:-}" deno run --allow-net --allow-env --allow-read --allow-write scripts/verify-storage.ts); then
|
||||||
|
:
|
||||||
|
else
|
||||||
|
fail=1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "FAIL - deno no está en PATH; no se pudo probar el bucket"
|
||||||
|
fail=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
if [[ "$fail" == "0" ]]; then
|
||||||
|
echo "Conectividad de este ambiente: OK"
|
||||||
|
else
|
||||||
|
echo "Hay fallos de conectividad -- no desplegar la API contra este ambiente todavía."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
@ -81,8 +81,8 @@ En Coolify el servicio de PANELS se llama **`web-panel`** (FQDN ej. `panels.mrde
|
||||||
|
|
||||||
## Paso a paso en Coolify
|
## Paso a paso en Coolify
|
||||||
|
|
||||||
1. **Provisionar Postgres y Redis** como recursos gestionados de Coolify para el ambiente (uno de cada, no por módulo).
|
1. **Provisionar Postgres, Redis y bucket** — 1 de cada **por ambiente** (no por módulo; no compartir prod con staging).
|
||||||
2. **Aprovisionar roles/esquemas/ACLs**: correr los scripts de [`db/provision/`](../db/provision/README.md) contra ese Postgres/Redis (una vez, desde tu máquina o un job manual -- Coolify no lo hace por ti).
|
2. **Crear accesos y validar**: `./db/provision/create-accesses.sh --apply --verify --out .env.<env>.local` contra esos hosts (ver [`db/provision/README.md`](../db/provision/README.md)). Coolify no crea los roles `panels_*` ni los ACL de Redis solo.
|
||||||
3. **Aplicar Liquibase** (paso explícito, NO ocurre al arrancar la app): `./db/update.sh all --context-filter='!dev'` con las credenciales `_owner`. En staging/producción, **nunca** olvidar el `--context-filter` -- sin él, Liquibase corre TAMBIÉN los changesets de demo (`context=dev`).
|
3. **Aplicar Liquibase** (paso explícito, NO ocurre al arrancar la app): `./db/update.sh all --context-filter='!dev'` con las credenciales `_owner`. En staging/producción, **nunca** olvidar el `--context-filter` -- sin él, Liquibase corre TAMBIÉN los changesets de demo (`context=dev`).
|
||||||
4. **Bootstrap del primer admin**: `deno run ... api/scripts/bootstrap-admin.ts platform` y `... tenant --tenant-id=... --company-code=...` (ver `db/README.md`).
|
4. **Bootstrap del primer admin**: `deno run ... api/scripts/bootstrap-admin.ts platform` y `... tenant --tenant-id=... --company-code=...` (ver `db/README.md`).
|
||||||
5. **Push** este repo (sin `.env` ni `data/`).
|
5. **Push** este repo (sin `.env` ni `data/`).
|
||||||
|
|
@ -91,7 +91,7 @@ En Coolify el servicio de PANELS se llama **`web-panel`** (FQDN ej. `panels.mrde
|
||||||
8. **Dominios:** `web-panel` → panels; `web-saas` → saas; `api` sin FQDN (proxy `/v1`).
|
8. **Dominios:** `web-panel` → panels; `web-saas` → saas; `api` sin FQDN (proxy `/v1`).
|
||||||
9. Cargar en Coolify todas las variables **obligatorias** de la tabla de arriba + `COOKIE_SECURE=true`.
|
9. Cargar en Coolify todas las variables **obligatorias** de la tabla de arriba + `COOKIE_SECURE=true`.
|
||||||
10. Deploy.
|
10. Deploy.
|
||||||
11. Verificar `https://app…/v1/health` → debe responder `{"ok":true,"core":true,"platform":true,"redis":{"iam":true,"core":true}}`. Si algo es `false`, la app ni siquiera debería haber arrancado (fail-fast, Fase 7).
|
11. Verificar `https://app…/v1/health` → `ok`, `core`, `platform`, `redis.iam`, `redis.core` y `storage` (en prod `backend:"s3"`). Si algo falla, la API no arranca.
|
||||||
12. Login SaaS `admin` / tu `SEED_PASSWORD`.
|
12. Login SaaS `admin` / tu `SEED_PASSWORD`.
|
||||||
13. SMTP en `/smtp` o por `SMTP_*`.
|
13. SMTP en `/smtp` o por `SMTP_*`.
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue