188 lines
8.3 KiB
Markdown
188 lines
8.3 KiB
Markdown
# Migrare MinIO: local multi-bucket → cluster extern single-bucket
|
|
|
|
**Autor**: refactor 2026-04-25
|
|
**Branch**: `feat/minio-cluster-single-bucket`
|
|
**Scop**: mutarea fișierelor user-uploaded de pe `staging-dataLayer-minio` (bucket-per-user) pe cluster extern managed (`<minio-host>`, single bucket `didi-prod` cu prefix-uri).
|
|
|
|
---
|
|
|
|
## De ce single-bucket
|
|
|
|
Cluster extern (4 noduri MinIO `minio1..4.<minio-domain>`, EC:2, 466 GiB utilizabili) ne dă credentiale `didi-prod` cu **`s3:*` doar pe bucket-ul `didi-prod`**. Nu putem crea bucket-uri (testat: 3/3 încercări `Access Denied`).
|
|
|
|
Soluția: păstrăm conceptul "namespace per user" dar ca **prefix** în bucket-ul fix:
|
|
|
|
```
|
|
ÎNAINTE (local, multi-bucket): DUPĂ (cluster, single-bucket):
|
|
user-3/ didi-prod/
|
|
images/ users/3/
|
|
videos/ images/
|
|
audio-files/ videos/
|
|
user-19/ audio-files/
|
|
images/ users/19/
|
|
uploads/ images/
|
|
image-files/ didi-prod/uploads/ ← legacy fallback
|
|
didi-prod/image-files/ ← legacy fallback
|
|
```
|
|
|
|
Numele de prefix-uri sistem (`uploads`, `image-files`, `audio-files`, `video-files`, `text-files`, `document-files`, `pipeline-artifacts`) sunt **identice cu numele bucket-urilor vechi** — astfel URL-urile vechi rămân interpretabile de proxy fără DB rewrite.
|
|
|
|
---
|
|
|
|
## Compatibilitate URL — backward compat
|
|
|
|
URL-urile vechi din `bos_analysis.analysis_session.input_media_url` și PG history pointează la formate vechi:
|
|
|
|
```
|
|
https://didi365.eu/storage/user-3/images/abc.jpg ← format vechi user
|
|
https://didi365.eu/api/v3/media/file/user-3/images/abc.jpg ← proxy vechi
|
|
http://staging-dataLayer-minio:9000/image-files/foo.jpg ← bucket sistem
|
|
```
|
|
|
|
Codul rezolvă toate aceste forme la canonic `didi-prod/<full-path>`:
|
|
|
|
| URL primit | Resolved bucket | Resolved key |
|
|
|---|---|---|
|
|
| `user-3/images/abc.jpg` | `didi-prod` | `users/3/images/abc.jpg` |
|
|
| `image-files/foo.jpg` | `didi-prod` | `image-files/foo.jpg` |
|
|
| `didi-prod/users/3/images/abc.jpg` | `didi-prod` | `users/3/images/abc.jpg` (passthrough) |
|
|
|
|
Implementat în:
|
|
- `didiFramework/src/config/minio.ts` → `resolveBucketRequest(bucket, key)`
|
|
- `agent-v3/src/shared/media/media-service.ts` → inline în `proxyFile()`
|
|
|
|
---
|
|
|
|
## Modificări în cod (branch `feat/minio-cluster-single-bucket`)
|
|
|
|
### didiFramework
|
|
|
|
| Fișier | Schimbare |
|
|
|---|---|
|
|
| `src/config/minio.ts` | Refactor complet: `BUCKET` fix, `resolveBucketRequest()`, `userObjectKey()`, no-op `ensureBucket()` în cluster mode, `createUserBucket()` lazy (doar logging) |
|
|
| `src/routes/auth.ts` (endpoint `/internal/get-bucket-info`) | Returnează `bucketName=didi-prod` + `folder=users/{id}/{mimeFolder}` în loc de `bucketName=user-{id}` + `folder={mimeFolder}` |
|
|
| `src/routes/uploads.ts` | **Nemodificat** — funcționează prin backward compat (caller pasează `bucket=image-files`, layer-ul îl rezolvă) |
|
|
| `sql/migrations/010_add_user_storage_quota.sql` | NEW — adaugă `storage_used_bytes` + `storage_limit_bytes` la `internet_user` (înlocuiește bucket tags) |
|
|
| `docker-compose.yml` | Env vars cu fallback `${MINIO_*:-default}` |
|
|
|
|
### agent-v3
|
|
|
|
| Fișier | Schimbare |
|
|
|---|---|
|
|
| `src/shared/media/media-service.ts` | `ensureBucket()` no-op în single-bucket mode. `proxyFile()` rezolvă legacy bucket → cluster prefix. Ownership check acceptă atât `bucket=user-{N}` cât și prefix `users/{N}/`. `uploadFile()` folosește `MINIO_BUCKET` env ca fallback |
|
|
| `docker-compose.yml` | `MINIO_BUCKET`, `MINIO_USE_SSL`, `MINIO_PUBLIC_ENDPOINT` adăugate la `x-worker-env` cu fallback empty (= legacy multi-bucket) |
|
|
|
|
### Scripts
|
|
|
|
| Fișier | Schimbare |
|
|
|---|---|
|
|
| `scripts/minio-switch.sh` | NEW — swap între local/cluster. Sourceaza credentiale din `.cluster-credentials.env` |
|
|
| `scripts/.cluster-credentials.env` | + `DIDI_MINIO_ACCESS_KEY` / `DIDI_MINIO_SECRET_KEY` / `DIDI_MINIO_DEV_*` |
|
|
|
|
---
|
|
|
|
## Cluster extern — date de conectare
|
|
|
|
```
|
|
S3 API: http://<minio-host>:9000 (VIP 10.11.10.128)
|
|
Console: http://<minio-console-host>:9001 (VIP 10.11.10.129)
|
|
Region: us-east-1
|
|
Path style: REQUIRED (forcePathStyle în SDK)
|
|
TLS: false acum (HTTPS în curând prin HAProxy)
|
|
|
|
User PROD: didi-prod / 627074a6ceb8d8531feffaf4ee9cc45007350c58
|
|
User DEV: didi-dev / 6f51811a4a157cb8f8d0d4376877552b780a06b8
|
|
Bucket PROD: didi-prod (full s3:* doar pe bucket-ul propriu)
|
|
Bucket DEV: didi-dev (idem)
|
|
```
|
|
|
|
Infrastructură: 4 noduri Proxmox extern (`minio1..4.<minio-domain>`, IPs `10.11.10.124-127`), erasure coding EC:2, HAProxy + keepalived VRRP, 466 GiB utilizabili.
|
|
|
|
---
|
|
|
|
## Cutover plan — NEEXECUTAT încă
|
|
|
|
Codul e gata pe branch, **runtime-ul production rămâne pe local MinIO** până când executăm cutover-ul deliberat.
|
|
|
|
### Pre-cutover checklist
|
|
|
|
1. ✓ Backup complet creat: `/home/admin365/backups/pre-minio-refactor-20260425-0935/` (545 MB cu cod + git bundle + 476 MB MinIO data)
|
|
2. ✓ Branch `feat/minio-cluster-single-bucket` cu refactor + tests
|
|
3. ✓ Cluster `didi-dev` validat (write/list/delete cu prefix-uri noi)
|
|
4. ⏳ Apply migration 010 pe PG cluster
|
|
5. ⏳ Smoke test cu un user fictiv pe `didi-dev` (upload + analiză + URL public)
|
|
|
|
### Cutover steps
|
|
|
|
```bash
|
|
# 1. Apply DB migration
|
|
docker exec didi-framework node -e "
|
|
const fs = require('fs');
|
|
const { Pool } = require('pg');
|
|
const pool = new Pool({ host: '10.11.50.167', port: 5000, user: 'bos_interface', password: 'interface', database: 'DIDI' });
|
|
const sql = fs.readFileSync('/app/sql/migrations/010_add_user_storage_quota.sql', 'utf8');
|
|
pool.query(sql).then(() => console.log('migration 010 applied')).catch(console.error).finally(() => pool.end());
|
|
"
|
|
|
|
# 2. Mirror data local → cluster (~2 min pe LAN)
|
|
mc alias set local http://staging-dataLayer-minio:9000 minioadmin minio123
|
|
mc alias set prod http://<minio-host>:9000 didi-prod '<secret>'
|
|
|
|
# Per-user buckets → users/{id}/ prefix
|
|
for u in $(mc ls local/ | grep -oE 'user-[0-9]+/' | tr -d '/'); do
|
|
id=${u#user-}
|
|
echo "mirroring $u → users/$id/"
|
|
mc mirror local/$u prod/didi-prod/users/$id/
|
|
done
|
|
|
|
# Sistem buckets → identical names ca prefix top-level
|
|
for b in uploads image-files audio-files video-files text-files document-files pipeline-artifacts backups; do
|
|
mc mirror local/$b prod/didi-prod/$b/
|
|
done
|
|
|
|
# 3. Switch env la cluster
|
|
./backend/services/orchestration-layer/scripts/minio-switch.sh cluster
|
|
# (rescrie .env, restart didi-framework + agent-v3)
|
|
|
|
# 4. Smoke test
|
|
curl -X POST https://didi365.eu/api/v3/pipeline/analyze ...
|
|
|
|
# 5. Monitor 24-48h. Local stays running ca fallback.
|
|
```
|
|
|
|
### Rollback (30s)
|
|
|
|
```bash
|
|
./backend/services/orchestration-layer/scripts/minio-switch.sh local
|
|
```
|
|
|
|
Local container rămâne pornit și păstrează datele. Singura pierdere: upload-uri făcute între cutover și rollback nu sunt pe local. Acceptabil pentru fereastra de monitoring.
|
|
|
|
---
|
|
|
|
## Riscuri cunoscute
|
|
|
|
1. **Quota** — la cutover, `storage_used_bytes` în PG e populat din migration 010 doar dacă userul are deja subscription cu `storage_limit_gb`. Pentru calcul real folosit, după cutover rulez:
|
|
```sql
|
|
-- Reconciliere usage cu MinIO real (job periodic recomandat)
|
|
SELECT internet_user_id FROM bos_sysadmin.internet_user WHERE storage_used_bytes = 0;
|
|
-- Pentru fiecare → mc du didi-prod/users/{id}/ → UPDATE
|
|
```
|
|
|
|
2. **TLS în curând** — endpoint va deveni `https://<minio-host>` (Let's Encrypt prin HAProxy). La acel moment: `MINIO_USE_SSL=true` + endpoint nou.
|
|
|
|
3. **Cleanup local cluster** — local `staging-dataLayer-minio` are 7 alte proiecte (lege365-corpus, ml-models, voice-recordings, publisher-assets, rafai-prod). NU le ștergem la cleanup.
|
|
|
|
4. **HAProxy failover** — VIP `10.11.10.128` rulează pe `cai1`. La failover poate fi o pauză de 2-5s. SDK-ul minio-js are retry implicit, dar la upload-uri mari (>100MB) merită setări explicite.
|
|
|
|
---
|
|
|
|
## Status build
|
|
|
|
```
|
|
✓ tsc didiFramework — exit 0
|
|
✓ tsc agent-v3 — zero erori noi (doar pre-existente pe `pg` module)
|
|
✓ docker compose build — toate imagini OK
|
|
✓ minio-switch.sh status — funcțional
|
|
✓ Test write/list/delete pe didi-dev cu prefix-uri noi — validat
|
|
```
|