feat: consolidate docker-compose.yml for baked-in images
Remove all bind-mounted config files and init containers. Services reduced from 7 to 5. All configuration via environment variables.
This commit is contained in:
46
.env.example
46
.env.example
@@ -1,9 +1,18 @@
|
|||||||
# Cameleer SaaS Environment Variables
|
# Cameleer SaaS — Environment Configuration
|
||||||
# Copy to .env and fill in values
|
# Copy to .env and fill in values for production
|
||||||
|
|
||||||
# Application version
|
# Image version
|
||||||
VERSION=latest
|
VERSION=latest
|
||||||
|
|
||||||
|
# Public access
|
||||||
|
PUBLIC_HOST=localhost
|
||||||
|
PUBLIC_PROTOCOL=https
|
||||||
|
|
||||||
|
# Ports
|
||||||
|
HTTP_PORT=80
|
||||||
|
HTTPS_PORT=443
|
||||||
|
LOGTO_CONSOLE_PORT=3002
|
||||||
|
|
||||||
# PostgreSQL
|
# PostgreSQL
|
||||||
POSTGRES_USER=cameleer
|
POSTGRES_USER=cameleer
|
||||||
POSTGRES_PASSWORD=change_me_in_production
|
POSTGRES_PASSWORD=change_me_in_production
|
||||||
@@ -12,19 +21,24 @@ POSTGRES_DB=cameleer_saas
|
|||||||
# ClickHouse
|
# ClickHouse
|
||||||
CLICKHOUSE_PASSWORD=change_me_in_production
|
CLICKHOUSE_PASSWORD=change_me_in_production
|
||||||
|
|
||||||
# Public domain (used by Traefik, Logto, and SaaS provisioning)
|
# Admin user (created by bootstrap)
|
||||||
PUBLIC_HOST=localhost
|
SAAS_ADMIN_USER=admin
|
||||||
PUBLIC_PROTOCOL=https
|
SAAS_ADMIN_PASS=change_me_in_production
|
||||||
|
|
||||||
# Logto Identity Provider (infrastructure — used by logto-bootstrap init container)
|
# TLS (leave empty for self-signed)
|
||||||
LOGTO_ENDPOINT=http://logto:3001
|
# NODE_TLS_REJECT=0 # Set to 1 when using real certificates
|
||||||
LOGTO_DB_PASSWORD=change_me_in_production
|
# CERT_FILE=
|
||||||
|
# KEY_FILE=
|
||||||
|
# CA_FILE=
|
||||||
|
|
||||||
# SaaS Identity (Logto M2M credentials — usually auto-provisioned by bootstrap)
|
# Vendor account (optional)
|
||||||
CAMELEER_SAAS_IDENTITY_M2MCLIENTID=
|
VENDOR_SEED_ENABLED=false
|
||||||
CAMELEER_SAAS_IDENTITY_M2MCLIENTSECRET=
|
# VENDOR_USER=vendor
|
||||||
CAMELEER_SAAS_IDENTITY_SPACLIENTID=
|
# VENDOR_PASS=change_me
|
||||||
|
|
||||||
# SaaS Provisioning
|
# Docker images (override for custom registries)
|
||||||
CAMELEER_SAAS_PROVISIONING_SERVERIMAGE=gitea.siegeln.net/cameleer/cameleer3-server:latest
|
# TRAEFIK_IMAGE=gitea.siegeln.net/cameleer/cameleer-traefik
|
||||||
CAMELEER_SAAS_PROVISIONING_SERVERUIIMAGE=gitea.siegeln.net/cameleer/cameleer3-server-ui:latest
|
# POSTGRES_IMAGE=gitea.siegeln.net/cameleer/cameleer-postgres
|
||||||
|
# CLICKHOUSE_IMAGE=gitea.siegeln.net/cameleer/cameleer-clickhouse
|
||||||
|
# LOGTO_IMAGE=gitea.siegeln.net/cameleer/cameleer-logto
|
||||||
|
# CAMELEER_IMAGE=gitea.siegeln.net/cameleer/cameleer-saas
|
||||||
|
|||||||
@@ -1,58 +1,11 @@
|
|||||||
services:
|
services:
|
||||||
traefik-certs:
|
traefik:
|
||||||
image: alpine:latest
|
image: ${TRAEFIK_IMAGE:-gitea.siegeln.net/cameleer/cameleer-traefik}:${VERSION:-latest}
|
||||||
restart: "no"
|
restart: unless-stopped
|
||||||
entrypoint: ["sh", "-c"]
|
ports:
|
||||||
command:
|
- "${HTTP_PORT:-80}:80"
|
||||||
- |
|
- "${HTTPS_PORT:-443}:443"
|
||||||
if [ -f /certs/cert.pem ]; then
|
- "${LOGTO_CONSOLE_PORT:-3002}:3002"
|
||||||
echo "Certs already exist, skipping"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Option 1: User-supplied certificate
|
|
||||||
if [ -n "$$CERT_FILE" ] && [ -n "$$KEY_FILE" ]; then
|
|
||||||
apk add --no-cache openssl >/dev/null 2>&1
|
|
||||||
cp "$$CERT_FILE" /certs/cert.pem
|
|
||||||
cp "$$KEY_FILE" /certs/key.pem
|
|
||||||
if [ -n "$$CA_FILE" ]; then
|
|
||||||
cp "$$CA_FILE" /certs/ca.pem
|
|
||||||
fi
|
|
||||||
# Validate: key matches cert
|
|
||||||
CERT_MOD=$$(openssl x509 -noout -modulus -in /certs/cert.pem 2>/dev/null | md5sum)
|
|
||||||
KEY_MOD=$$(openssl rsa -noout -modulus -in /certs/key.pem 2>/dev/null | md5sum)
|
|
||||||
if [ "$$CERT_MOD" != "$$KEY_MOD" ]; then
|
|
||||||
echo "ERROR: Certificate and key do not match!"
|
|
||||||
rm -f /certs/cert.pem /certs/key.pem /certs/ca.pem
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
SELF_SIGNED=false
|
|
||||||
echo "Installed user-supplied certificate"
|
|
||||||
else
|
|
||||||
# Option 2: Generate self-signed
|
|
||||||
apk add --no-cache openssl >/dev/null 2>&1
|
|
||||||
openssl req -x509 -newkey rsa:4096 \
|
|
||||||
-keyout /certs/key.pem -out /certs/cert.pem \
|
|
||||||
-days 365 -nodes \
|
|
||||||
-subj "/CN=$$PUBLIC_HOST" \
|
|
||||||
-addext "subjectAltName=DNS:$$PUBLIC_HOST,DNS:*.$$PUBLIC_HOST"
|
|
||||||
SELF_SIGNED=true
|
|
||||||
echo "Generated self-signed cert for $$PUBLIC_HOST"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Write metadata for SaaS app to seed DB
|
|
||||||
SUBJECT=$$(openssl x509 -noout -subject -in /certs/cert.pem 2>/dev/null | sed 's/subject=//')
|
|
||||||
FINGERPRINT=$$(openssl x509 -noout -fingerprint -sha256 -in /certs/cert.pem 2>/dev/null | sed 's/.*=//')
|
|
||||||
NOT_BEFORE=$$(openssl x509 -noout -startdate -in /certs/cert.pem 2>/dev/null | sed 's/notBefore=//')
|
|
||||||
NOT_AFTER=$$(openssl x509 -noout -enddate -in /certs/cert.pem 2>/dev/null | sed 's/notAfter=//')
|
|
||||||
HAS_CA=false
|
|
||||||
[ -f /certs/ca.pem ] && HAS_CA=true
|
|
||||||
cat > /certs/meta.json <<METAEOF
|
|
||||||
{"subject":"$$SUBJECT","fingerprint":"$$FINGERPRINT","selfSigned":$$SELF_SIGNED,"hasCa":$$HAS_CA,"notBefore":"$$NOT_BEFORE","notAfter":"$$NOT_AFTER"}
|
|
||||||
METAEOF
|
|
||||||
mkdir -p /certs/staged /certs/prev
|
|
||||||
chmod 775 /certs /certs/staged /certs/prev
|
|
||||||
chmod 660 /certs/*.pem 2>/dev/null || true
|
|
||||||
environment:
|
environment:
|
||||||
PUBLIC_HOST: ${PUBLIC_HOST:-localhost}
|
PUBLIC_HOST: ${PUBLIC_HOST:-localhost}
|
||||||
CERT_FILE: ${CERT_FILE:-}
|
CERT_FILE: ${CERT_FILE:-}
|
||||||
@@ -60,28 +13,13 @@ services:
|
|||||||
CA_FILE: ${CA_FILE:-}
|
CA_FILE: ${CA_FILE:-}
|
||||||
volumes:
|
volumes:
|
||||||
- certs:/certs
|
- certs:/certs
|
||||||
|
|
||||||
traefik:
|
|
||||||
image: traefik:v3
|
|
||||||
restart: unless-stopped
|
|
||||||
depends_on:
|
|
||||||
traefik-certs:
|
|
||||||
condition: service_completed_successfully
|
|
||||||
ports:
|
|
||||||
- "80:80"
|
|
||||||
- "443:443"
|
|
||||||
- "3002:3002"
|
|
||||||
volumes:
|
|
||||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
- ./traefik.yml:/etc/traefik/traefik.yml:ro
|
|
||||||
- ./docker/traefik-dynamic.yml:/etc/traefik/dynamic.yml:ro
|
|
||||||
- certs:/etc/traefik/certs:ro
|
|
||||||
networks:
|
networks:
|
||||||
- cameleer
|
- cameleer
|
||||||
- cameleer-traefik
|
- cameleer-traefik
|
||||||
|
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres:16-alpine
|
image: ${POSTGRES_IMAGE:-gitea.siegeln.net/cameleer/cameleer-postgres}:${VERSION:-latest}
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_DB: ${POSTGRES_DB:-cameleer_saas}
|
POSTGRES_DB: ${POSTGRES_DB:-cameleer_saas}
|
||||||
@@ -89,7 +27,6 @@ services:
|
|||||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-cameleer_dev}
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-cameleer_dev}
|
||||||
volumes:
|
volumes:
|
||||||
- pgdata:/var/lib/postgresql/data
|
- pgdata:/var/lib/postgresql/data
|
||||||
- ./docker/init-databases.sh:/docker-entrypoint-initdb.d/init-databases.sh:ro
|
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-cameleer} -d ${POSTGRES_DB:-cameleer_saas}"]
|
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-cameleer} -d ${POSTGRES_DB:-cameleer_saas}"]
|
||||||
interval: 5s
|
interval: 5s
|
||||||
@@ -98,54 +35,37 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- cameleer
|
- cameleer
|
||||||
|
|
||||||
|
clickhouse:
|
||||||
|
image: ${CLICKHOUSE_IMAGE:-gitea.siegeln.net/cameleer/cameleer-clickhouse}:${VERSION:-latest}
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD:-cameleer_ch}
|
||||||
|
volumes:
|
||||||
|
- chdata:/var/lib/clickhouse
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "clickhouse-client --password ${CLICKHOUSE_PASSWORD:-cameleer_ch} --query 'SELECT 1'"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
labels:
|
||||||
|
- prometheus.scrape=true
|
||||||
|
- prometheus.path=/metrics
|
||||||
|
- prometheus.port=9363
|
||||||
|
networks:
|
||||||
|
- cameleer
|
||||||
|
|
||||||
logto:
|
logto:
|
||||||
image: ${LOGTO_IMAGE:-gitea.siegeln.net/cameleer/cameleer-logto}:${VERSION:-latest}
|
image: ${LOGTO_IMAGE:-gitea.siegeln.net/cameleer/cameleer-logto}:${VERSION:-latest}
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
depends_on:
|
depends_on:
|
||||||
postgres:
|
postgres:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
entrypoint: ["sh", "-c", "npm run cli db seed -- --swe && npm start"]
|
|
||||||
environment:
|
environment:
|
||||||
DB_URL: postgres://${POSTGRES_USER:-cameleer}:${POSTGRES_PASSWORD:-cameleer_dev}@postgres:5432/logto
|
DB_URL: postgres://${POSTGRES_USER:-cameleer}:${POSTGRES_PASSWORD:-cameleer_dev}@postgres:5432/logto
|
||||||
ENDPOINT: ${PUBLIC_PROTOCOL:-https}://${PUBLIC_HOST:-localhost}
|
ENDPOINT: ${PUBLIC_PROTOCOL:-https}://${PUBLIC_HOST:-localhost}
|
||||||
ADMIN_ENDPOINT: ${PUBLIC_PROTOCOL:-https}://${PUBLIC_HOST:-localhost}:3002
|
ADMIN_ENDPOINT: ${PUBLIC_PROTOCOL:-https}://${PUBLIC_HOST:-localhost}:${LOGTO_CONSOLE_PORT:-3002}
|
||||||
TRUST_PROXY_HEADER: 1
|
TRUST_PROXY_HEADER: 1
|
||||||
NODE_TLS_REJECT_UNAUTHORIZED: "0" # dev only — accept self-signed cert for internal OIDC discovery
|
NODE_TLS_REJECT_UNAUTHORIZED: "${NODE_TLS_REJECT:-0}"
|
||||||
healthcheck:
|
|
||||||
test: ["CMD-SHELL", "node -e \"require('http').get('http://localhost:3001/oidc/.well-known/openid-configuration', r => process.exit(r.statusCode === 200 ? 0 : 1)).on('error', () => process.exit(1))\""]
|
|
||||||
interval: 5s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 30
|
|
||||||
start_period: 15s
|
|
||||||
labels:
|
|
||||||
- traefik.enable=true
|
|
||||||
- traefik.http.routers.logto.rule=PathPrefix(`/`)
|
|
||||||
- traefik.http.routers.logto.priority=1
|
|
||||||
- traefik.http.routers.logto.entrypoints=websecure
|
|
||||||
- traefik.http.routers.logto.tls=true
|
|
||||||
- traefik.http.routers.logto.service=logto
|
|
||||||
- traefik.http.routers.logto.middlewares=logto-cors
|
|
||||||
- traefik.http.middlewares.logto-cors.headers.accessControlAllowOriginList=${PUBLIC_PROTOCOL:-https}://${PUBLIC_HOST:-localhost}:3002
|
|
||||||
- traefik.http.middlewares.logto-cors.headers.accessControlAllowMethods=GET,POST,PUT,PATCH,DELETE,OPTIONS
|
|
||||||
- traefik.http.middlewares.logto-cors.headers.accessControlAllowHeaders=Authorization,Content-Type
|
|
||||||
- traefik.http.middlewares.logto-cors.headers.accessControlAllowCredentials=true
|
|
||||||
- traefik.http.services.logto.loadbalancer.server.port=3001
|
|
||||||
- traefik.http.routers.logto-console.rule=PathPrefix(`/`)
|
|
||||||
- traefik.http.routers.logto-console.entrypoints=admin-console
|
|
||||||
- traefik.http.routers.logto-console.tls=true
|
|
||||||
- traefik.http.routers.logto-console.service=logto-console
|
|
||||||
- traefik.http.services.logto-console.loadbalancer.server.port=3002
|
|
||||||
networks:
|
|
||||||
- cameleer
|
|
||||||
|
|
||||||
logto-bootstrap:
|
|
||||||
image: postgres:16-alpine
|
|
||||||
depends_on:
|
|
||||||
logto:
|
|
||||||
condition: service_healthy
|
|
||||||
restart: "no"
|
|
||||||
entrypoint: ["sh", "/scripts/logto-bootstrap.sh"]
|
|
||||||
environment:
|
|
||||||
LOGTO_ENDPOINT: http://logto:3001
|
LOGTO_ENDPOINT: http://logto:3001
|
||||||
LOGTO_ADMIN_ENDPOINT: http://logto:3002
|
LOGTO_ADMIN_ENDPOINT: http://logto:3002
|
||||||
LOGTO_PUBLIC_ENDPOINT: ${PUBLIC_PROTOCOL:-https}://${PUBLIC_HOST:-localhost}
|
LOGTO_PUBLIC_ENDPOINT: ${PUBLIC_PROTOCOL:-https}://${PUBLIC_HOST:-localhost}
|
||||||
@@ -157,8 +77,34 @@ services:
|
|||||||
PG_DB_SAAS: ${POSTGRES_DB:-cameleer_saas}
|
PG_DB_SAAS: ${POSTGRES_DB:-cameleer_saas}
|
||||||
SAAS_ADMIN_USER: ${SAAS_ADMIN_USER:-admin}
|
SAAS_ADMIN_USER: ${SAAS_ADMIN_USER:-admin}
|
||||||
SAAS_ADMIN_PASS: ${SAAS_ADMIN_PASS:-admin}
|
SAAS_ADMIN_PASS: ${SAAS_ADMIN_PASS:-admin}
|
||||||
|
VENDOR_SEED_ENABLED: "${VENDOR_SEED_ENABLED:-false}"
|
||||||
|
VENDOR_USER: ${VENDOR_USER:-vendor}
|
||||||
|
VENDOR_PASS: ${VENDOR_PASS:-vendor}
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "node -e \"require('http').get('http://localhost:3001/oidc/.well-known/openid-configuration', r => process.exit(r.statusCode === 200 ? 0 : 1)).on('error', () => process.exit(1))\" && test -f /data/logto-bootstrap.json"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 60
|
||||||
|
start_period: 30s
|
||||||
|
labels:
|
||||||
|
- traefik.enable=true
|
||||||
|
- traefik.http.routers.logto.rule=PathPrefix(`/`)
|
||||||
|
- traefik.http.routers.logto.priority=1
|
||||||
|
- traefik.http.routers.logto.entrypoints=websecure
|
||||||
|
- traefik.http.routers.logto.tls=true
|
||||||
|
- traefik.http.routers.logto.service=logto
|
||||||
|
- traefik.http.routers.logto.middlewares=logto-cors
|
||||||
|
- "traefik.http.middlewares.logto-cors.headers.accessControlAllowOriginList=${PUBLIC_PROTOCOL:-https}://${PUBLIC_HOST:-localhost}:${LOGTO_CONSOLE_PORT:-3002}"
|
||||||
|
- traefik.http.middlewares.logto-cors.headers.accessControlAllowMethods=GET,POST,PUT,PATCH,DELETE,OPTIONS
|
||||||
|
- traefik.http.middlewares.logto-cors.headers.accessControlAllowHeaders=Authorization,Content-Type
|
||||||
|
- traefik.http.middlewares.logto-cors.headers.accessControlAllowCredentials=true
|
||||||
|
- traefik.http.services.logto.loadbalancer.server.port=3001
|
||||||
|
- traefik.http.routers.logto-console.rule=PathPrefix(`/`)
|
||||||
|
- traefik.http.routers.logto-console.entrypoints=admin-console
|
||||||
|
- traefik.http.routers.logto-console.tls=true
|
||||||
|
- traefik.http.routers.logto-console.service=logto-console
|
||||||
|
- traefik.http.services.logto-console.loadbalancer.server.port=3002
|
||||||
volumes:
|
volumes:
|
||||||
- ./docker/logto-bootstrap.sh:/scripts/logto-bootstrap.sh:ro
|
|
||||||
- bootstrapdata:/data
|
- bootstrapdata:/data
|
||||||
networks:
|
networks:
|
||||||
- cameleer
|
- cameleer
|
||||||
@@ -167,10 +113,8 @@ services:
|
|||||||
image: ${CAMELEER_IMAGE:-gitea.siegeln.net/cameleer/cameleer-saas}:${VERSION:-latest}
|
image: ${CAMELEER_IMAGE:-gitea.siegeln.net/cameleer/cameleer-saas}:${VERSION:-latest}
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
depends_on:
|
depends_on:
|
||||||
postgres:
|
logto:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
logto-bootstrap:
|
|
||||||
condition: service_completed_successfully
|
|
||||||
volumes:
|
volumes:
|
||||||
- bootstrapdata:/data/bootstrap:ro
|
- bootstrapdata:/data/bootstrap:ro
|
||||||
- certs:/certs
|
- certs:/certs
|
||||||
@@ -193,28 +137,6 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- cameleer
|
- cameleer
|
||||||
|
|
||||||
clickhouse:
|
|
||||||
image: clickhouse/clickhouse-server:latest
|
|
||||||
restart: unless-stopped
|
|
||||||
environment:
|
|
||||||
CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD:-cameleer_ch}
|
|
||||||
volumes:
|
|
||||||
- chdata:/var/lib/clickhouse
|
|
||||||
- ./docker/clickhouse-init.sql:/docker-entrypoint-initdb.d/init.sql:ro
|
|
||||||
- ./docker/clickhouse-users.xml:/etc/clickhouse-server/users.d/default-user.xml
|
|
||||||
- ./docker/clickhouse-config.xml:/etc/clickhouse-server/config.d/prometheus.xml:ro
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD-SHELL", "clickhouse-client --password ${CLICKHOUSE_PASSWORD:-cameleer_ch} --query 'SELECT 1'"]
|
|
||||||
interval: 10s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 3
|
|
||||||
labels:
|
|
||||||
- prometheus.scrape=true
|
|
||||||
- prometheus.path=/metrics
|
|
||||||
- prometheus.port=9363
|
|
||||||
networks:
|
|
||||||
- cameleer
|
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
cameleer:
|
cameleer:
|
||||||
driver: bridge
|
driver: bridge
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
<clickhouse>
|
|
||||||
<prometheus>
|
|
||||||
<endpoint>/metrics</endpoint>
|
|
||||||
<port>9363</port>
|
|
||||||
<metrics>true</metrics>
|
|
||||||
<events>true</events>
|
|
||||||
<asynchronous_metrics>true</asynchronous_metrics>
|
|
||||||
</prometheus>
|
|
||||||
</clickhouse>
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
CREATE DATABASE IF NOT EXISTS cameleer;
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
<clickhouse>
|
|
||||||
<users>
|
|
||||||
<default>
|
|
||||||
<password from_env="CLICKHOUSE_PASSWORD" />
|
|
||||||
<networks>
|
|
||||||
<ip>::/0</ip>
|
|
||||||
</networks>
|
|
||||||
</default>
|
|
||||||
</users>
|
|
||||||
</clickhouse>
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
set -e
|
|
||||||
|
|
||||||
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
|
|
||||||
CREATE DATABASE logto;
|
|
||||||
CREATE DATABASE cameleer3;
|
|
||||||
GRANT ALL PRIVILEGES ON DATABASE logto TO $POSTGRES_USER;
|
|
||||||
GRANT ALL PRIVILEGES ON DATABASE cameleer3 TO $POSTGRES_USER;
|
|
||||||
EOSQL
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
http:
|
|
||||||
routers:
|
|
||||||
root-redirect:
|
|
||||||
rule: "Path(`/`)"
|
|
||||||
priority: 100
|
|
||||||
entryPoints:
|
|
||||||
- websecure
|
|
||||||
tls: {}
|
|
||||||
middlewares:
|
|
||||||
- root-to-platform
|
|
||||||
service: saas@docker
|
|
||||||
middlewares:
|
|
||||||
root-to-platform:
|
|
||||||
redirectRegex:
|
|
||||||
regex: "^(https?://[^/]+)/?$"
|
|
||||||
replacement: "${1}/platform/"
|
|
||||||
permanent: false
|
|
||||||
|
|
||||||
tls:
|
|
||||||
stores:
|
|
||||||
default:
|
|
||||||
defaultCertificate:
|
|
||||||
certFile: /etc/traefik/certs/cert.pem
|
|
||||||
keyFile: /etc/traefik/certs/key.pem
|
|
||||||
@@ -1,135 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
set -e
|
|
||||||
|
|
||||||
# Cameleer SaaS — Vendor Seed Script
|
|
||||||
# Creates the saas-vendor global role and vendor user.
|
|
||||||
# Run ONCE on the hosted SaaS environment AFTER standard bootstrap.
|
|
||||||
# NOT part of docker-compose.yml — invoked manually or by CI.
|
|
||||||
|
|
||||||
LOGTO_ENDPOINT="${LOGTO_ENDPOINT:-http://logto:3001}"
|
|
||||||
MGMT_API_RESOURCE="https://default.logto.app/api"
|
|
||||||
API_RESOURCE_INDICATOR="https://api.cameleer.local"
|
|
||||||
PG_HOST="${PG_HOST:-postgres}"
|
|
||||||
PG_USER="${PG_USER:-cameleer}"
|
|
||||||
PG_DB_LOGTO="logto"
|
|
||||||
|
|
||||||
# Vendor credentials (override via env vars)
|
|
||||||
VENDOR_USER="${VENDOR_USER:-vendor}"
|
|
||||||
VENDOR_PASS="${VENDOR_PASS:-vendor}"
|
|
||||||
VENDOR_NAME="${VENDOR_NAME:-SaaS Vendor}"
|
|
||||||
|
|
||||||
log() { echo "[vendor-seed] $1"; }
|
|
||||||
pgpass() { PGPASSWORD="${PG_PASSWORD:-cameleer_dev}"; export PGPASSWORD; }
|
|
||||||
|
|
||||||
# Install jq + curl
|
|
||||||
apk add --no-cache jq curl >/dev/null 2>&1
|
|
||||||
|
|
||||||
# ============================================================
|
|
||||||
# Get Management API token
|
|
||||||
# ============================================================
|
|
||||||
|
|
||||||
log "Reading M2M credentials from bootstrap file..."
|
|
||||||
BOOTSTRAP_FILE="/data/logto-bootstrap.json"
|
|
||||||
if [ ! -f "$BOOTSTRAP_FILE" ]; then
|
|
||||||
log "ERROR: Bootstrap file not found at $BOOTSTRAP_FILE — run standard bootstrap first"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
M2M_ID=$(jq -r '.m2mClientId' "$BOOTSTRAP_FILE")
|
|
||||||
M2M_SECRET=$(jq -r '.m2mClientSecret' "$BOOTSTRAP_FILE")
|
|
||||||
|
|
||||||
if [ -z "$M2M_ID" ] || [ "$M2M_ID" = "null" ] || [ -z "$M2M_SECRET" ] || [ "$M2M_SECRET" = "null" ]; then
|
|
||||||
log "ERROR: M2M credentials not found in bootstrap file"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
log "Getting Management API token..."
|
|
||||||
TOKEN_RESPONSE=$(curl -s -X POST "${LOGTO_ENDPOINT}/oidc/token" \
|
|
||||||
-H "Content-Type: application/x-www-form-urlencoded" \
|
|
||||||
-d "grant_type=client_credentials&client_id=${M2M_ID}&client_secret=${M2M_SECRET}&resource=${MGMT_API_RESOURCE}&scope=all")
|
|
||||||
TOKEN=$(echo "$TOKEN_RESPONSE" | jq -r '.access_token' 2>/dev/null)
|
|
||||||
[ -z "$TOKEN" ] || [ "$TOKEN" = "null" ] && { log "ERROR: Failed to get token"; exit 1; }
|
|
||||||
log "Got Management API token."
|
|
||||||
|
|
||||||
api_get() { curl -s -H "Authorization: Bearer $TOKEN" "${LOGTO_ENDPOINT}${1}" 2>/dev/null || echo "[]"; }
|
|
||||||
api_post() { curl -s -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d "$2" "${LOGTO_ENDPOINT}${1}" 2>/dev/null || true; }
|
|
||||||
|
|
||||||
# ============================================================
|
|
||||||
# Create saas-vendor global role
|
|
||||||
# ============================================================
|
|
||||||
|
|
||||||
log "Checking for saas-vendor role..."
|
|
||||||
EXISTING_ROLES=$(api_get "/api/roles")
|
|
||||||
VENDOR_ROLE_ID=$(echo "$EXISTING_ROLES" | jq -r '.[] | select(.name == "saas-vendor" and .type == "User") | .id')
|
|
||||||
|
|
||||||
if [ -n "$VENDOR_ROLE_ID" ]; then
|
|
||||||
log "saas-vendor role exists: $VENDOR_ROLE_ID"
|
|
||||||
else
|
|
||||||
# Collect all API resource scope IDs
|
|
||||||
EXISTING_RESOURCES=$(api_get "/api/resources")
|
|
||||||
API_RESOURCE_ID=$(echo "$EXISTING_RESOURCES" | jq -r ".[] | select(.indicator == \"$API_RESOURCE_INDICATOR\") | .id")
|
|
||||||
ALL_SCOPE_IDS=$(api_get "/api/resources/$API_RESOURCE_ID/scopes" | jq '[.[].id]')
|
|
||||||
|
|
||||||
log "Creating saas-vendor role with all scopes..."
|
|
||||||
VENDOR_ROLE_RESPONSE=$(api_post "/api/roles" "{
|
|
||||||
\"name\": \"saas-vendor\",
|
|
||||||
\"description\": \"SaaS vendor — full platform control across all tenants\",
|
|
||||||
\"type\": \"User\",
|
|
||||||
\"scopeIds\": $ALL_SCOPE_IDS
|
|
||||||
}")
|
|
||||||
VENDOR_ROLE_ID=$(echo "$VENDOR_ROLE_RESPONSE" | jq -r '.id')
|
|
||||||
log "Created saas-vendor role: $VENDOR_ROLE_ID"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ============================================================
|
|
||||||
# Create vendor user
|
|
||||||
# ============================================================
|
|
||||||
|
|
||||||
log "Checking for vendor user '$VENDOR_USER'..."
|
|
||||||
VENDOR_USER_ID=$(api_get "/api/users?search=$VENDOR_USER" | jq -r ".[] | select(.username == \"$VENDOR_USER\") | .id")
|
|
||||||
|
|
||||||
if [ -n "$VENDOR_USER_ID" ]; then
|
|
||||||
log "Vendor user exists: $VENDOR_USER_ID"
|
|
||||||
else
|
|
||||||
log "Creating vendor user '$VENDOR_USER'..."
|
|
||||||
VENDOR_RESPONSE=$(api_post "/api/users" "{
|
|
||||||
\"username\": \"$VENDOR_USER\",
|
|
||||||
\"password\": \"$VENDOR_PASS\",
|
|
||||||
\"name\": \"$VENDOR_NAME\"
|
|
||||||
}")
|
|
||||||
VENDOR_USER_ID=$(echo "$VENDOR_RESPONSE" | jq -r '.id')
|
|
||||||
log "Created vendor user: $VENDOR_USER_ID"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Assign saas-vendor role
|
|
||||||
if [ -n "$VENDOR_ROLE_ID" ] && [ "$VENDOR_ROLE_ID" != "null" ]; then
|
|
||||||
api_post "/api/users/$VENDOR_USER_ID/roles" "{\"roleIds\": [\"$VENDOR_ROLE_ID\"]}" >/dev/null 2>&1
|
|
||||||
log "Assigned saas-vendor role."
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ============================================================
|
|
||||||
# Add vendor to all existing organizations with owner role
|
|
||||||
# ============================================================
|
|
||||||
|
|
||||||
log "Adding vendor to all organizations..."
|
|
||||||
ORG_OWNER_ROLE_ID=$(api_get "/api/organization-roles" | jq -r '.[] | select(.name == "owner") | .id')
|
|
||||||
ORGS=$(api_get "/api/organizations")
|
|
||||||
ORG_COUNT=$(echo "$ORGS" | jq 'length')
|
|
||||||
|
|
||||||
for i in $(seq 0 $((ORG_COUNT - 1))); do
|
|
||||||
ORG_ID=$(echo "$ORGS" | jq -r ".[$i].id")
|
|
||||||
ORG_NAME=$(echo "$ORGS" | jq -r ".[$i].name")
|
|
||||||
api_post "/api/organizations/$ORG_ID/users" "{\"userIds\": [\"$VENDOR_USER_ID\"]}" >/dev/null 2>&1
|
|
||||||
if [ -n "$ORG_OWNER_ROLE_ID" ] && [ "$ORG_OWNER_ROLE_ID" != "null" ]; then
|
|
||||||
curl -s -X PUT -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
|
|
||||||
-d "{\"organizationRoleIds\": [\"$ORG_OWNER_ROLE_ID\"]}" \
|
|
||||||
"${LOGTO_ENDPOINT}/api/organizations/$ORG_ID/users/$VENDOR_USER_ID/roles" >/dev/null 2>&1
|
|
||||||
fi
|
|
||||||
log " Added to org '$ORG_NAME' ($ORG_ID) with owner role."
|
|
||||||
done
|
|
||||||
|
|
||||||
log ""
|
|
||||||
log "=== Vendor seed complete! ==="
|
|
||||||
log " Vendor user: $VENDOR_USER / $VENDOR_PASS"
|
|
||||||
log " Role: saas-vendor (global) + owner (in all orgs)"
|
|
||||||
log " This user has platform:admin scope and cross-tenant access."
|
|
||||||
24
traefik.yml
24
traefik.yml
@@ -1,24 +0,0 @@
|
|||||||
api:
|
|
||||||
dashboard: false
|
|
||||||
|
|
||||||
entryPoints:
|
|
||||||
web:
|
|
||||||
address: ":80"
|
|
||||||
http:
|
|
||||||
redirections:
|
|
||||||
entryPoint:
|
|
||||||
to: websecure
|
|
||||||
scheme: https
|
|
||||||
websecure:
|
|
||||||
address: ":443"
|
|
||||||
admin-console:
|
|
||||||
address: ":3002"
|
|
||||||
|
|
||||||
providers:
|
|
||||||
docker:
|
|
||||||
endpoint: "unix:///var/run/docker.sock"
|
|
||||||
exposedByDefault: false
|
|
||||||
network: cameleer
|
|
||||||
file:
|
|
||||||
filename: /etc/traefik/dynamic.yml
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user