diff --git a/.env.example b/.env.example index 940b99b..e3bc0c4 100644 --- a/.env.example +++ b/.env.example @@ -158,10 +158,12 @@ PAPERLESS_DB_PASSWORD=paperless PAPERLESS_SECRET_KEY=CHANGE_ME_IN_PRODUCTION # ---------------------------------- -# Object Storage (MinIO / S3) — carrier label files +# Object Storage (S3-compatible; Garage in the dev stack) — labels and product images # ---------------------------------- -MINIO_ROOT_USER=opentaberna -MINIO_ROOT_PASSWORD=opentaberna_secret +# The dev stack's Garage creates its access key from STORAGE_ACCESS_KEY and +# STORAGE_SECRET_KEY below. It also needs a cluster secret of 32 random bytes: +# openssl rand -hex 32 +GARAGE_RPC_SECRET= STORAGE_ENDPOINT_URL=http://localhost:9000 STORAGE_ACCESS_KEY=opentaberna STORAGE_SECRET_KEY=opentaberna_secret diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 70e2b07..6cfd6bb 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -63,6 +63,7 @@ jobs: KEYCLOAK_CLIENT_SECRET=opentaberna-secret STRIPE_SECRET_KEY=sk_test_placeholder STRIPE_WEBHOOK_SECRET=whsec_placeholder + GARAGE_RPC_SECRET=$(openssl rand -hex 32) LOG_LEVEL=${{ github.event.inputs.log_level || 'INFO' }} EOF echo "✅ Test environment file created" @@ -70,15 +71,15 @@ jobs: - name: 🔧 Install system tools run: sudo apt-get install -y postgresql-client redis-tools - - name: 🐳 Start services (DB, Redis, Keycloak, MinIO) + - name: 🐳 Start services (DB, Redis, Keycloak, Garage) run: | - mkdir -p postgres_data redis_data minio_data - docker compose -f docker-compose.dev.yml up opentaberna-db opentaberna-redis opentaberna-keycloak opentaberna-minio -d + mkdir -p postgres_data redis_data garage_data + docker compose -f docker-compose.dev.yml up opentaberna-db opentaberna-redis opentaberna-keycloak opentaberna-garage -d echo "✅ Services started" - name: 🗄️ Wait for backing services run: | - echo "⏳ Waiting for DB, Redis, Keycloak and MinIO..." + echo "⏳ Waiting for DB, Redis, Keycloak and Garage..." for i in {1..40}; do echo "Attempt $i/40..." @@ -86,11 +87,11 @@ jobs: pg_isready -h localhost -p 5432 -U opentaberna 2>/dev/null && PG_READY=1 && echo "✅ Postgres ready" redis-cli -h localhost -p 6379 ping 2>/dev/null | grep -q PONG && REDIS_READY=1 && echo "✅ Redis ready" curl -sf http://localhost:8080/realms/opentaberna/.well-known/openid-configuration > /dev/null 2>&1 && KC_READY=1 && echo "✅ Keycloak ready" - # MinIO backs the product-image endpoints; without it those tests + # Garage backs the product-image endpoints; without it those tests # fail with a storage error rather than anything meaningful. - curl -sf http://localhost:9000/minio/health/live > /dev/null 2>&1 && MINIO_READY=1 && echo "✅ MinIO ready" + curl -sf http://localhost:3903/health > /dev/null 2>&1 && GARAGE_READY=1 && echo "✅ Garage ready" - [ "$PG_READY" = "1" ] && [ "$REDIS_READY" = "1" ] && [ "$KC_READY" = "1" ] && [ "$MINIO_READY" = "1" ] && echo "✅ All backing services ready!" && exit 0 + [ "$PG_READY" = "1" ] && [ "$REDIS_READY" = "1" ] && [ "$KC_READY" = "1" ] && [ "$GARAGE_READY" = "1" ] && echo "✅ All backing services ready!" && exit 0 sleep 5 done diff --git a/.gitignore b/.gitignore index a331f5c..d93973d 100644 --- a/.gitignore +++ b/.gitignore @@ -146,4 +146,4 @@ cython_debug/ # postgresql db directory postgres_data/ redis_data/ -minio_data/ \ No newline at end of file +garage_data/ \ No newline at end of file diff --git a/README.md b/README.md index 72d4aee..ebee844 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,12 @@ The development stack also starts a Stripe CLI listener. Set a Stripe test-mode the API and provides its generated webhook signing secret automatically. No manual `stripe listen` process or `STRIPE_WEBHOOK_SECRET` copy is required. +Object storage is [Garage](https://garagehq.deuxfleurs.fr), an S3-compatible store +(MinIO's images are no longer published). It needs `GARAGE_RPC_SECRET` in `.env` — +generate one with `openssl rand -hex 32`. On start it creates its access key from +`STORAGE_ACCESS_KEY`/`STORAGE_SECRET_KEY`; the API creates its buckets itself. The S3 +API is on `http://localhost:9000`, its health check on `http://localhost:3903/health`. + # Pipelines This FastAPI can be build and tested via GitHub workflows. There are two available workflows: diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 49facf1..616f1ff 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -10,7 +10,7 @@ services: REDIS_URL: redis://opentaberna-redis:6379/0 KEYCLOAK_URL: http://opentaberna-keycloak:8080 KEYCLOAK_PUBLIC_URL: http://localhost:8080 - STORAGE_ENDPOINT_URL: http://opentaberna-minio:9000 + STORAGE_ENDPOINT_URL: http://opentaberna-garage:3900 OTEL_EXPORTER_OTLP_ENDPOINT: http://opentaberna-otel-collector:4318 OTEL_SERVICE_NAME: opentaberna-api PAPERLESS_URL: http://opentaberna-paperless:8000 @@ -33,7 +33,7 @@ services: condition: service_started opentaberna-keycloak: condition: service_started - opentaberna-minio: + opentaberna-garage: condition: service_started opentaberna-stripe-listener: condition: service_started @@ -153,25 +153,30 @@ services: retries: 10 start_period: 60s - opentaberna-minio: - image: minio/minio:latest - command: server /data --console-address ":9001" + opentaberna-garage: + image: dxflrs/garage:v2.4.1 + # --single-node lays out a one-node cluster and --default-access-key creates + # the API's key on start, so the store needs no manual setup. The API + # creates its buckets itself. + command: /garage server --single-node --default-access-key environment: - MINIO_ROOT_USER: ${MINIO_ROOT_USER:-opentaberna} - MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:-opentaberna_secret} + GARAGE_RPC_SECRET: ${GARAGE_RPC_SECRET:?set GARAGE_RPC_SECRET in .env, generate one with openssl rand -hex 32} + GARAGE_DEFAULT_ACCESS_KEY: ${STORAGE_ACCESS_KEY:-opentaberna} + GARAGE_DEFAULT_SECRET_KEY: ${STORAGE_SECRET_KEY:-opentaberna_secret} volumes: - - ./minio_data:/data + - ./docker/garage/garage.toml:/etc/garage.toml:ro + - ./garage_data:/var/lib/garage ports: - - "9000:9000" # S3 API - - "9001:9001" # MinIO Web Console + - "9000:3900" # S3 API + - "3903:3903" # Admin API (GET /health) restart: unless-stopped - container_name: opentaberna-minio + container_name: opentaberna-garage healthcheck: - test: ["CMD-SHELL", "curl -fsS http://127.0.0.1:9000/minio/health/live || exit 1"] + test: ["CMD", "/garage", "status"] interval: 30s timeout: 10s retries: 5 - start_period: 20s + start_period: 10s opentaberna-worker: build: @@ -182,7 +187,7 @@ services: env_file: .env environment: REDIS_URL: redis://opentaberna-redis:6379/0 - STORAGE_ENDPOINT_URL: http://opentaberna-minio:9000 + STORAGE_ENDPOINT_URL: http://opentaberna-garage:3900 OTEL_EXPORTER_OTLP_ENDPOINT: http://opentaberna-otel-collector:4318 OTEL_SERVICE_NAME: opentaberna-worker restart: unless-stopped @@ -198,7 +203,7 @@ services: condition: service_healthy opentaberna-redis: condition: service_healthy - opentaberna-minio: + opentaberna-garage: condition: service_healthy opentaberna-mail: diff --git a/docker/garage/garage.toml b/docker/garage/garage.toml new file mode 100644 index 0000000..0c27184 --- /dev/null +++ b/docker/garage/garage.toml @@ -0,0 +1,18 @@ +# Single-node Garage for local development and CI. Not a production config: +# one copy of every object, no redundancy. +metadata_dir = "/var/lib/garage/meta" +data_dir = "/var/lib/garage/data" +db_engine = "sqlite" +replication_factor = 1 + +rpc_bind_addr = "[::]:3901" +rpc_public_addr = "127.0.0.1:3901" + +[s3_api] +# The API's boto client signs for us-east-1 by default; Garage rejects +# requests signed for any region other than this one. +s3_region = "us-east-1" +api_bind_addr = "[::]:3900" + +[admin] +api_bind_addr = "[::]:3903" diff --git a/docs/config.md b/docs/config.md index 58602da..7c0152e 100644 --- a/docs/config.md +++ b/docs/config.md @@ -201,17 +201,17 @@ could otherwise drive the back office. | `smtp_password` | str | Empty | SMTP password | | `email_from` | str | `noreply@opentaberna.local` | Envelope sender address | -### Object Storage (MinIO / S3) +### Object Storage (S3-compatible) | Setting | Type | Default | Description | |---------|------|---------|-------------| | `storage_endpoint_url` | str | `http://localhost:9000` | S3-compatible endpoint | -| `storage_access_key` | str | `minioadmin` | Access key | -| `storage_secret_key` | str | `minioadmin` | Secret key | -| `storage_bucket_labels` | str | `labels` | Bucket holding carrier labels | +| `storage_access_key` | str | `opentaberna` | Access key | +| `storage_secret_key` | str | `opentaberna_secret` | Secret key | +| `storage_bucket_labels` | str | `shipping-labels` | Bucket holding carrier labels | | `storage_bucket_items` | str | `item-images` | Bucket holding product images | | `storage_max_image_bytes` | int | `5242880` | Largest product image accepted (5 MB) | -| `storage_region` | str | `us-east-1` | Region name | +| `storage_region` | str | `us-east-1` | Region name; must match the store's region (`s3_region` in `docker/garage/garage.toml`) | ### DHL (Carrier Adapter) diff --git a/src/app/shared/config/settings.py b/src/app/shared/config/settings.py index 5930c4d..18969f7 100644 --- a/src/app/shared/config/settings.py +++ b/src/app/shared/config/settings.py @@ -248,18 +248,18 @@ class Settings(BaseSettings): description="How long (in minutes) a stock reservation is held before it expires", ) - # MinIO / S3 object storage + # S3-compatible object storage (Garage in the dev stack) storage_endpoint_url: str = Field( default="http://localhost:9000", - description="S3-compatible storage endpoint URL (MinIO or AWS S3)", + description="S3-compatible storage endpoint URL", ) storage_access_key: str = Field( default="opentaberna", - description="Storage access key ID (MinIO root user or AWS access key)", + description="Storage access key ID", ) storage_secret_key: str = Field( default="opentaberna_secret", - description="Storage secret access key (MinIO root password or AWS secret key)", + description="Storage secret access key", ) storage_bucket_items: str = Field( default="item-images", @@ -278,7 +278,7 @@ class Settings(BaseSettings): ) storage_region: str = Field( default="us-east-1", - description="Storage region (MinIO ignores this; required by boto3 client)", + description="Storage region; must match the store's configured region", ) # DHL Parcel DE REST API