diff --git a/docker-compose.yml b/docker-compose.yml index 1be6fa9..98d2a28 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -80,7 +80,6 @@ services: VENDOR_SEED_ENABLED: "${VENDOR_SEED_ENABLED:-false}" VENDOR_USER: ${VENDOR_USER:-vendor} VENDOR_PASS: ${VENDOR_PASS:-vendor} - TENANT_ORG_NAME: ${TENANT_ORG_NAME:-} 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 diff --git a/docker/logto-bootstrap.sh b/docker/logto-bootstrap.sh index 332aa6c..0a83cf5 100644 --- a/docker/logto-bootstrap.sh +++ b/docker/logto-bootstrap.sh @@ -583,11 +583,9 @@ EOF chmod 644 "$BOOTSTRAP_FILE" # ============================================================ -# Phase 12: Deployment Mode (vendor or single-tenant) +# Phase 12: Vendor Seed (optional) # ============================================================ -TENANT_ORG_NAME="${TENANT_ORG_NAME:-}" - if [ "$VENDOR_SEED_ENABLED" = "true" ]; then log "" log "=== Phase 12a: Vendor Seed ===" @@ -688,53 +686,6 @@ if [ "$VENDOR_SEED_ENABLED" = "true" ]; then fi log "Vendor seed complete." - -elif [ -n "$TENANT_ORG_NAME" ]; then - log "" - log "=== Phase 12b: Single-Tenant Setup ===" - - # Create organization for the tenant - TENANT_SLUG=$(echo "$TENANT_ORG_NAME" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9-]/-/g; s/--*/-/g; s/^-//; s/-$//') - log "Creating organization '$TENANT_ORG_NAME' (slug: $TENANT_SLUG)..." - - EXISTING_ORG_ID=$(api_get "/api/organizations" | jq -r ".[] | select(.name == \"$TENANT_ORG_NAME\") | .id") - if [ -n "$EXISTING_ORG_ID" ]; then - log "Organization already exists: $EXISTING_ORG_ID" - TENANT_ORG_ID="$EXISTING_ORG_ID" - else - ORG_RESPONSE=$(api_post "/api/organizations" "{\"name\": \"$TENANT_ORG_NAME\"}") - TENANT_ORG_ID=$(echo "$ORG_RESPONSE" | jq -r '.id') - log "Created organization: $TENANT_ORG_ID" - fi - - # Add admin user to organization with owner role - if [ -n "$TENANT_ORG_ID" ] && [ "$TENANT_ORG_ID" != "null" ]; then - api_post "/api/organizations/$TENANT_ORG_ID/users" "{\"userIds\": [\"$ADMIN_USER_ID\"]}" >/dev/null 2>&1 - ORG_OWNER_ROLE_ID=$(api_get "/api/organization-roles" | jq -r '.[] | select(.name == "owner") | .id') - 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" $HOST_ARGS \ - -d "{\"organizationRoleIds\": [\"$ORG_OWNER_ROLE_ID\"]}" \ - "${LOGTO_ENDPOINT}/api/organizations/$TENANT_ORG_ID/users/$ADMIN_USER_ID/roles" >/dev/null 2>&1 - fi - log "Added admin user to organization with owner role." - - # Register OIDC redirect URIs for the tenant - TRAD_APP=$(api_get "/api/applications" | jq -r ".[] | select(.name == \"$TRAD_APP_NAME\") | .id") - if [ -n "$TRAD_APP" ] && [ "$TRAD_APP" != "null" ]; then - EXISTING_URIS=$(api_get "/api/applications/$TRAD_APP" | jq -r '.oidcClientMetadata.redirectUris') - NEW_URI="${PROTO}://${HOST}/t/${TENANT_SLUG}/oidc/callback" - if ! echo "$EXISTING_URIS" | jq -e ".[] | select(. == \"$NEW_URI\")" >/dev/null 2>&1; then - UPDATED_URIS=$(echo "$EXISTING_URIS" | jq ". + [\"$NEW_URI\"]") - api_patch "/api/applications/$TRAD_APP" "{\"oidcClientMetadata\": {\"redirectUris\": $UPDATED_URIS}}" >/dev/null 2>&1 - log "Registered OIDC redirect URI for tenant: $NEW_URI" - fi - fi - - # NOTE: Tenant DB record is created by the installer after Flyway migrations - # have run (the tenants table doesn't exist yet at bootstrap time). - fi - - log "Single-tenant setup complete." fi log "" diff --git a/installer/install.sh b/installer/install.sh index 9705f3a..a749369 100644 --- a/installer/install.sh +++ b/installer/install.sh @@ -25,6 +25,7 @@ DEFAULT_LOGTO_CONSOLE_EXPOSED="true" DEFAULT_VENDOR_ENABLED="false" DEFAULT_VENDOR_USER="vendor" DEFAULT_COMPOSE_PROJECT="cameleer-saas" +DEFAULT_COMPOSE_PROJECT_STANDALONE="cameleer" DEFAULT_DOCKER_SOCKET="/var/run/docker.sock" # --- Config values (set by args/env/config/prompts) --- @@ -48,6 +49,7 @@ _ENV_MONITORING_NETWORK="${MONITORING_NETWORK:-}" _ENV_COMPOSE_PROJECT="${COMPOSE_PROJECT:-}" _ENV_DOCKER_SOCKET="${DOCKER_SOCKET:-}" _ENV_NODE_TLS_REJECT="${NODE_TLS_REJECT:-}" +_ENV_DEPLOYMENT_MODE="${DEPLOYMENT_MODE:-}" INSTALL_DIR="" PUBLIC_HOST="" @@ -72,7 +74,7 @@ VERSION="" COMPOSE_PROJECT="" DOCKER_SOCKET="" NODE_TLS_REJECT="" -TENANT_ORG_NAME="" +DEPLOYMENT_MODE="" # --- State --- MODE="" # simple, expert, silent @@ -175,7 +177,9 @@ parse_args() { --compose-project) COMPOSE_PROJECT="$2"; shift ;; --docker-socket) DOCKER_SOCKET="$2"; shift ;; --node-tls-reject) NODE_TLS_REJECT="$2"; shift ;; - --tenant-org-name) TENANT_ORG_NAME="$2"; shift ;; + --deployment-mode) DEPLOYMENT_MODE="$2"; shift ;; + --server-admin-user) ADMIN_USER="$2"; shift ;; + --server-admin-password) ADMIN_PASS="$2"; shift ;; --reconfigure) RERUN_ACTION="reconfigure" ;; --reinstall) RERUN_ACTION="reinstall" ;; --confirm-destroy) CONFIRM_DESTROY=true ;; @@ -260,7 +264,7 @@ load_config_file() { compose_project) [ -z "$COMPOSE_PROJECT" ] && COMPOSE_PROJECT="$value" ;; docker_socket) [ -z "$DOCKER_SOCKET" ] && DOCKER_SOCKET="$value" ;; node_tls_reject) [ -z "$NODE_TLS_REJECT" ] && NODE_TLS_REJECT="$value" ;; - tenant_org_name) [ -z "$TENANT_ORG_NAME" ] && TENANT_ORG_NAME="$value" ;; + deployment_mode) [ -z "$DEPLOYMENT_MODE" ] && DEPLOYMENT_MODE="$value" ;; esac done < "$file" } @@ -289,6 +293,7 @@ load_env_overrides() { [ -z "$COMPOSE_PROJECT" ] && COMPOSE_PROJECT="$_ENV_COMPOSE_PROJECT" [ -z "$DOCKER_SOCKET" ] && DOCKER_SOCKET="$_ENV_DOCKER_SOCKET" [ -z "$NODE_TLS_REJECT" ] && NODE_TLS_REJECT="$_ENV_NODE_TLS_REJECT" + [ -z "$DEPLOYMENT_MODE" ] && DEPLOYMENT_MODE="$_ENV_DEPLOYMENT_MODE" } # --- Prerequisites --- @@ -329,7 +334,9 @@ check_prerequisites() { check_port_available "${HTTP_PORT:-$DEFAULT_HTTP_PORT}" "HTTP" check_port_available "${HTTPS_PORT:-$DEFAULT_HTTPS_PORT}" "HTTPS" - check_port_available "${LOGTO_CONSOLE_PORT:-$DEFAULT_LOGTO_CONSOLE_PORT}" "Logto Console" + if [ "$DEPLOYMENT_MODE" != "standalone" ]; then + check_port_available "${LOGTO_CONSOLE_PORT:-$DEFAULT_LOGTO_CONSOLE_PORT}" "Logto Console" + fi if [ $errors -gt 0 ]; then log_error "$errors prerequisite(s) not met. Please install missing dependencies and retry." @@ -430,19 +437,19 @@ run_simple_prompts() { echo "" echo " Deployment mode:" - echo " [1] Multi-tenant vendor — admin manages platform, creates tenants on demand" - echo " [2] Single tenant — set up one tenant for immediate use" + echo " [1] Multi-tenant vendor — manage platform, provision tenants on demand" + echo " [2] Single-tenant — one server instance, local auth, no identity provider" echo "" local deploy_choice read -rp " Select mode [1]: " deploy_choice case "${deploy_choice:-1}" in 2) + DEPLOYMENT_MODE="standalone" VENDOR_ENABLED="false" - prompt TENANT_ORG_NAME "Organization / tenant name" "" ;; *) + DEPLOYMENT_MODE="saas" VENDOR_ENABLED="true" - TENANT_ORG_NAME="" ;; esac } @@ -463,24 +470,28 @@ run_expert_prompts() { prompt_password CLICKHOUSE_PASSWORD "ClickHouse password" "" fi - echo "" - if prompt_yesno "Enable vendor account?"; then - VENDOR_ENABLED="true" - prompt VENDOR_USER "Vendor username" "${VENDOR_USER:-$DEFAULT_VENDOR_USER}" - if prompt_yesno "Auto-generate vendor password?" "y"; then - VENDOR_PASS="" + if [ "$DEPLOYMENT_MODE" = "saas" ]; then + echo "" + if prompt_yesno "Enable vendor account?"; then + VENDOR_ENABLED="true" + prompt VENDOR_USER "Vendor username" "${VENDOR_USER:-$DEFAULT_VENDOR_USER}" + if prompt_yesno "Auto-generate vendor password?" "y"; then + VENDOR_PASS="" + else + prompt_password VENDOR_PASS "Vendor password" "" + fi else - prompt_password VENDOR_PASS "Vendor password" "" + VENDOR_ENABLED="false" fi - else - VENDOR_ENABLED="false" fi echo "" echo -e "${BOLD} Networking:${NC}" prompt HTTP_PORT "HTTP port" "${HTTP_PORT:-$DEFAULT_HTTP_PORT}" prompt HTTPS_PORT "HTTPS port" "${HTTPS_PORT:-$DEFAULT_HTTPS_PORT}" - prompt LOGTO_CONSOLE_PORT "Logto admin console port" "${LOGTO_CONSOLE_PORT:-$DEFAULT_LOGTO_CONSOLE_PORT}" + if [ "$DEPLOYMENT_MODE" = "saas" ]; then + prompt LOGTO_CONSOLE_PORT "Logto admin console port" "${LOGTO_CONSOLE_PORT:-$DEFAULT_LOGTO_CONSOLE_PORT}" + fi echo "" echo -e "${BOLD} Docker:${NC}" @@ -488,18 +499,21 @@ run_expert_prompts() { prompt COMPOSE_PROJECT "Compose project name" "${COMPOSE_PROJECT:-$DEFAULT_COMPOSE_PROJECT}" prompt DOCKER_SOCKET "Docker socket path" "${DOCKER_SOCKET:-$DEFAULT_DOCKER_SOCKET}" - echo "" - echo -e "${BOLD} Logto:${NC}" - if prompt_yesno "Expose Logto admin console externally?" "y"; then - LOGTO_CONSOLE_EXPOSED="true" - else - LOGTO_CONSOLE_EXPOSED="false" + if [ "$DEPLOYMENT_MODE" = "saas" ]; then + echo "" + echo -e "${BOLD} Logto:${NC}" + if prompt_yesno "Expose Logto admin console externally?" "y"; then + LOGTO_CONSOLE_EXPOSED="true" + else + LOGTO_CONSOLE_EXPOSED="false" + fi fi } # --- Config merge and validation --- merge_config() { + : "${DEPLOYMENT_MODE:=saas}" : "${INSTALL_DIR:=$DEFAULT_INSTALL_DIR}" : "${PUBLIC_HOST:=localhost}" : "${PUBLIC_PROTOCOL:=$DEFAULT_PUBLIC_PROTOCOL}" @@ -512,17 +526,24 @@ merge_config() { : "${VENDOR_ENABLED:=$DEFAULT_VENDOR_ENABLED}" : "${VENDOR_USER:=$DEFAULT_VENDOR_USER}" : "${VERSION:=$CAMELEER_DEFAULT_VERSION}" - : "${COMPOSE_PROJECT:=$DEFAULT_COMPOSE_PROJECT}" : "${DOCKER_SOCKET:=$DEFAULT_DOCKER_SOCKET}" + if [ "$DEPLOYMENT_MODE" = "standalone" ]; then + : "${COMPOSE_PROJECT:=$DEFAULT_COMPOSE_PROJECT_STANDALONE}" + else + : "${COMPOSE_PROJECT:=$DEFAULT_COMPOSE_PROJECT}" + fi + # Force lowercase hostname — Logto normalizes internally, case mismatch breaks JWT validation PUBLIC_HOST=$(echo "$PUBLIC_HOST" | tr '[:upper:]' '[:lower:]') - if [ -z "$NODE_TLS_REJECT" ]; then - if [ "$TLS_MODE" = "custom" ]; then - NODE_TLS_REJECT="1" - else - NODE_TLS_REJECT="0" + if [ "$DEPLOYMENT_MODE" != "standalone" ]; then + if [ -z "$NODE_TLS_REJECT" ]; then + if [ "$TLS_MODE" = "custom" ]; then + NODE_TLS_REJECT="1" + else + NODE_TLS_REJECT="0" + fi fi fi } @@ -545,7 +566,9 @@ validate_config() { fi fi - for port_var in HTTP_PORT HTTPS_PORT LOGTO_CONSOLE_PORT; do + local port_vars="HTTP_PORT HTTPS_PORT" + [ "$DEPLOYMENT_MODE" != "standalone" ] && port_vars="HTTP_PORT HTTPS_PORT LOGTO_CONSOLE_PORT" + for port_var in $port_vars; do local port_val eval "port_val=\$$port_var" if ! echo "$port_val" | grep -qE '^[0-9]+$' || [ "$port_val" -lt 1 ] || [ "$port_val" -gt 65535 ]; then @@ -595,6 +618,44 @@ copy_certs() { generate_env_file() { local f="$INSTALL_DIR/.env" + + if [ "$DEPLOYMENT_MODE" = "standalone" ]; then + cat > "$f" << EOF +# Cameleer Server Configuration (standalone) +# Generated by installer v${CAMELEER_INSTALLER_VERSION} on $(date -u '+%Y-%m-%d %H:%M:%S UTC') + +VERSION=${VERSION} +PUBLIC_HOST=${PUBLIC_HOST} +PUBLIC_PROTOCOL=${PUBLIC_PROTOCOL} +HTTP_PORT=${HTTP_PORT} +HTTPS_PORT=${HTTPS_PORT} + +# PostgreSQL +POSTGRES_USER=cameleer +POSTGRES_PASSWORD=${POSTGRES_PASSWORD} +POSTGRES_DB=cameleer3 + +# ClickHouse +CLICKHOUSE_PASSWORD=${CLICKHOUSE_PASSWORD} + +# Server admin +SERVER_ADMIN_USER=${ADMIN_USER} +SERVER_ADMIN_PASS=${ADMIN_PASS} + +# Docker +DOCKER_SOCKET=${DOCKER_SOCKET} +DOCKER_GID=$(stat -c '%g' "${DOCKER_SOCKET}" 2>/dev/null || echo "0") +EOF + if [ "$TLS_MODE" = "custom" ]; then + echo "CERT_FILE=/user-certs/cert.pem" >> "$f" + echo "KEY_FILE=/user-certs/key.pem" >> "$f" + [ -n "$CA_FILE" ] && echo "CA_FILE=/user-certs/ca.pem" >> "$f" + fi + log_info "Generated .env" + cp "$f" "$INSTALL_DIR/.env.bak" + return + fi + cat > "$f" << EOF # Cameleer SaaS Configuration # Generated by installer v${CAMELEER_INSTALLER_VERSION} on $(date -u '+%Y-%m-%d %H:%M:%S UTC') @@ -644,9 +705,6 @@ VENDOR_SEED_ENABLED=${VENDOR_ENABLED} VENDOR_USER=${VENDOR_USER} VENDOR_PASS=${VENDOR_PASS:-} -# Single-tenant org (when vendor is disabled) -TENANT_ORG_NAME=${TENANT_ORG_NAME:-} - # Docker DOCKER_SOCKET=${DOCKER_SOCKET} DOCKER_GID=$(stat -c '%g' "${DOCKER_SOCKET}" 2>/dev/null || echo "0") @@ -661,6 +719,10 @@ EOF } generate_compose_file() { + if [ "$DEPLOYMENT_MODE" = "standalone" ]; then + generate_compose_file_standalone + return + fi local f="$INSTALL_DIR/docker-compose.yml" : > "$f" @@ -796,7 +858,6 @@ EOF VENDOR_SEED_ENABLED: "${VENDOR_SEED_ENABLED:-false}" VENDOR_USER: ${VENDOR_USER:-vendor} VENDOR_PASS: ${VENDOR_PASS:-vendor} - TENANT_ORG_NAME: ${TENANT_ORG_NAME:-} 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 @@ -920,6 +981,208 @@ EOF log_info "Generated docker-compose.yml" } +generate_compose_file_standalone() { + local f="$INSTALL_DIR/docker-compose.yml" + : > "$f" + + cat >> "$f" << 'COMPOSEEOF' +# Cameleer Server (standalone) +# Generated by Cameleer installer — do not edit manually + +services: + traefik: + image: ${TRAEFIK_IMAGE:-gitea.siegeln.net/cameleer/cameleer-traefik}:${VERSION:-latest} + restart: unless-stopped + ports: + - "${HTTP_PORT:-80}:80" + - "${HTTPS_PORT:-443}:443" + environment: + PUBLIC_HOST: ${PUBLIC_HOST:-localhost} + CERT_FILE: ${CERT_FILE:-} + KEY_FILE: ${KEY_FILE:-} + CA_FILE: ${CA_FILE:-} + volumes: + - certs:/certs + - ${DOCKER_SOCKET:-/var/run/docker.sock}:/var/run/docker.sock:ro + - ./traefik-dynamic.yml:/etc/traefik/dynamic.yml:ro +COMPOSEEOF + + if [ "$TLS_MODE" = "custom" ]; then + echo " - ./certs:/user-certs:ro" >> "$f" + fi + + cat >> "$f" << 'COMPOSEEOF' + networks: + - cameleer + - cameleer-traefik +COMPOSEEOF + + if [ -n "$MONITORING_NETWORK" ]; then + echo " - ${MONITORING_NETWORK}" >> "$f" + fi + + cat >> "$f" << 'COMPOSEEOF' + + postgres: + image: postgres:16-alpine + restart: unless-stopped + environment: + POSTGRES_DB: ${POSTGRES_DB:-cameleer3} + POSTGRES_USER: ${POSTGRES_USER:-cameleer} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + volumes: + - pgdata:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER:-cameleer} -d $${POSTGRES_DB:-cameleer3}"] + interval: 5s + timeout: 5s + retries: 5 + networks: + - cameleer +COMPOSEEOF + + if [ -n "$MONITORING_NETWORK" ]; then + echo " - ${MONITORING_NETWORK}" >> "$f" + fi + + cat >> "$f" << 'COMPOSEEOF' + + clickhouse: + image: ${CLICKHOUSE_IMAGE:-gitea.siegeln.net/cameleer/cameleer-clickhouse}:${VERSION:-latest} + restart: unless-stopped + environment: + CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD} + volumes: + - chdata:/var/lib/clickhouse + healthcheck: + test: ["CMD-SHELL", "clickhouse-client --password $${CLICKHOUSE_PASSWORD} --query 'SELECT 1'"] + interval: 10s + timeout: 5s + retries: 3 + networks: + - cameleer +COMPOSEEOF + + if [ -n "$MONITORING_NETWORK" ]; then + echo " - ${MONITORING_NETWORK}" >> "$f" + fi + + # Detect Docker socket GID + local docker_gid + docker_gid=$(stat -c '%g' "${DOCKER_SOCKET:-/var/run/docker.sock}" 2>/dev/null || echo "0") + + cat >> "$f" << COMPOSEEOF + + server: + image: \${SERVER_IMAGE:-gitea.siegeln.net/cameleer/cameleer3-server}:\${VERSION:-latest} + container_name: cameleer-server + restart: unless-stopped + depends_on: + postgres: + condition: service_healthy + environment: + CAMELEER_SERVER_TENANT_ID: default + SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/\${POSTGRES_DB:-cameleer3}?currentSchema=tenant_default + SPRING_DATASOURCE_USERNAME: \${POSTGRES_USER:-cameleer} + SPRING_DATASOURCE_PASSWORD: \${POSTGRES_PASSWORD} + CAMELEER_SERVER_CLICKHOUSE_URL: jdbc:clickhouse://clickhouse:8123/cameleer + CAMELEER_SERVER_CLICKHOUSE_USERNAME: default + CAMELEER_SERVER_CLICKHOUSE_PASSWORD: \${CLICKHOUSE_PASSWORD} + CAMELEER_SERVER_SECURITY_UIUSER: \${SERVER_ADMIN_USER:-admin} + CAMELEER_SERVER_SECURITY_UIPASSWORD: \${SERVER_ADMIN_PASS:-admin} + CAMELEER_SERVER_SECURITY_CORSALLOWEDORIGINS: \${PUBLIC_PROTOCOL:-https}://\${PUBLIC_HOST:-localhost} + CAMELEER_SERVER_RUNTIME_ENABLED: "true" + CAMELEER_SERVER_RUNTIME_SERVERURL: http://cameleer-server:8081 + CAMELEER_SERVER_RUNTIME_ROUTINGDOMAIN: \${PUBLIC_HOST:-localhost} + CAMELEER_SERVER_RUNTIME_ROUTINGMODE: path + CAMELEER_SERVER_RUNTIME_JARSTORAGEPATH: /data/jars + CAMELEER_SERVER_RUNTIME_DOCKERNETWORK: cameleer-apps + CAMELEER_SERVER_RUNTIME_JARDOCKERVOLUME: cameleer-jars + CAMELEER_SERVER_RUNTIME_BASEIMAGE: gitea.siegeln.net/cameleer/cameleer-runtime-base:\${VERSION:-latest} + labels: + - traefik.enable=true + - traefik.http.routers.server-api.rule=PathPrefix(\`/api\`) + - traefik.http.routers.server-api.entrypoints=websecure + - traefik.http.routers.server-api.tls=true + - traefik.http.services.server-api.loadbalancer.server.port=8081 + - traefik.docker.network=cameleer-traefik + healthcheck: + test: ["CMD-SHELL", "curl -sf http://localhost:8081/api/v1/health || exit 1"] + interval: 10s + timeout: 5s + retries: 30 + start_period: 30s + volumes: + - jars:/data/jars + - certs:/certs:ro + - \${DOCKER_SOCKET:-/var/run/docker.sock}:/var/run/docker.sock + group_add: + - "${docker_gid}" + networks: + - cameleer + - cameleer-traefik + - cameleer-apps + + server-ui: + image: \${SERVER_UI_IMAGE:-gitea.siegeln.net/cameleer/cameleer3-server-ui}:\${VERSION:-latest} + restart: unless-stopped + depends_on: + server: + condition: service_healthy + environment: + CAMELEER_API_URL: http://cameleer-server:8081 + BASE_PATH: "" + labels: + - traefik.enable=true + - traefik.http.routers.ui.rule=PathPrefix(\`/\`) + - traefik.http.routers.ui.priority=1 + - traefik.http.routers.ui.entrypoints=websecure + - traefik.http.routers.ui.tls=true + - traefik.http.services.ui.loadbalancer.server.port=80 + - traefik.docker.network=cameleer-traefik + networks: + - cameleer-traefik +COMPOSEEOF + + cat >> "$f" << 'COMPOSEEOF' + +volumes: + pgdata: + chdata: + certs: + jars: + +networks: + cameleer: + driver: bridge + cameleer-traefik: + name: cameleer-traefik + driver: bridge + cameleer-apps: + name: cameleer-apps + driver: bridge +COMPOSEEOF + + if [ -n "$MONITORING_NETWORK" ]; then + cat >> "$f" << EOF + ${MONITORING_NETWORK}: + external: true +EOF + fi + + # Generate standalone traefik dynamic config (overrides baked-in redirect) + cat > "$INSTALL_DIR/traefik-dynamic.yml" << 'TRAEFIKEOF' +tls: + stores: + default: + defaultCertificate: + certFile: /certs/cert.pem + keyFile: /certs/key.pem +TRAEFIKEOF + + log_info "Generated docker-compose.yml (standalone)" +} + # --- Docker operations --- docker_compose_pull() { @@ -1001,14 +1264,22 @@ verify_health() { [ $failed -eq 0 ] && \ wait_for_docker_healthy "ClickHouse" "clickhouse" 120 || failed=1 - [ $failed -eq 0 ] && \ - wait_for_docker_healthy "Logto + Bootstrap" "logto" 300 || failed=1 + if [ "$DEPLOYMENT_MODE" = "standalone" ]; then + [ $failed -eq 0 ] && \ + wait_for_docker_healthy "Cameleer Server" "server" 300 || failed=1 - [ $failed -eq 0 ] && \ - check_endpoint "Cameleer SaaS" "https://localhost:${HTTPS_PORT}/platform/api/config" 120 || failed=1 + [ $failed -eq 0 ] && \ + check_endpoint "Server UI" "https://localhost:${HTTPS_PORT}/" 60 || failed=1 + else + [ $failed -eq 0 ] && \ + wait_for_docker_healthy "Logto + Bootstrap" "logto" 300 || failed=1 - [ $failed -eq 0 ] && \ - check_endpoint "Traefik routing" "https://localhost:${HTTPS_PORT}/" 30 || failed=1 + [ $failed -eq 0 ] && \ + check_endpoint "Cameleer SaaS" "https://localhost:${HTTPS_PORT}/platform/api/config" 120 || failed=1 + + [ $failed -eq 0 ] && \ + check_endpoint "Traefik routing" "https://localhost:${HTTPS_PORT}/" 30 || failed=1 + fi echo "" if [ $failed -ne 0 ]; then @@ -1018,58 +1289,6 @@ verify_health() { log_success "All services healthy." } -# --- Single-tenant DB record --- - -setup_single_tenant_record() { - [ -z "$TENANT_ORG_NAME" ] && return 0 - - local slug - slug=$(echo "$TENANT_ORG_NAME" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9-]/-/g; s/--*/-/g; s/^-//; s/-$//') - - log_info "Creating single-tenant record for '$TENANT_ORG_NAME' (slug: $slug)..." - - # Check if tenant already exists - local existing - existing=$(cd "$INSTALL_DIR" && docker compose -p "$COMPOSE_PROJECT" exec -T postgres \ - psql -U "${POSTGRES_USER}" -d cameleer_saas -t -A -c \ - "SELECT id FROM tenants WHERE slug = '$slug';" 2>/dev/null) || true - - if [ -n "$existing" ]; then - printf " ${GREEN}[ok]${NC} Tenant record already exists: %s\n" "$slug" - return 0 - fi - - # Get Logto org ID from the logto database - local org_id - org_id=$(cd "$INSTALL_DIR" && docker compose -p "$COMPOSE_PROJECT" exec -T postgres \ - psql -U "${POSTGRES_USER}" -d logto -t -A -c \ - "SELECT id FROM organizations WHERE name = '$TENANT_ORG_NAME' AND tenant_id = 'default';" 2>/dev/null) || true - - if [ -z "$org_id" ]; then - log_warn "Could not find Logto organization for '$TENANT_ORG_NAME' — tenant record not created." - log_warn "Create the tenant manually via the vendor console." - return 0 - fi - - # Generate UUID and insert - local uuid - uuid=$(cat /proc/sys/kernel/random/uuid 2>/dev/null || python3 -c "import uuid; print(uuid.uuid4())" 2>/dev/null || true) - if [ -z "$uuid" ]; then - log_warn "Could not generate UUID — tenant record not created." - return 0 - fi - - if cd "$INSTALL_DIR" && docker compose -p "$COMPOSE_PROJECT" exec -T postgres \ - psql -U "${POSTGRES_USER}" -d cameleer_saas -c \ - "INSERT INTO tenants (id, name, slug, tier, status, logto_org_id, created_at, updated_at) - VALUES ('$uuid', '$TENANT_ORG_NAME', '$slug', 'STANDARD', 'PROVISIONING', '$org_id', NOW(), NOW());" >/dev/null 2>&1; then - printf " ${GREEN}[ok]${NC} Tenant record created: %s (status: PROVISIONING)\n" "$slug" - log_info "The SaaS app will provision the tenant's server automatically." - else - log_warn "Failed to create tenant record — create it manually via the vendor console." - fi -} - # --- Output file generation --- write_config_file() { @@ -1094,13 +1313,36 @@ version=${VERSION} compose_project=${COMPOSE_PROJECT} docker_socket=${DOCKER_SOCKET} node_tls_reject=${NODE_TLS_REJECT} -tenant_org_name=${TENANT_ORG_NAME} +deployment_mode=${DEPLOYMENT_MODE} EOF log_info "Saved installer config to cameleer.conf" } generate_credentials_file() { local f="$INSTALL_DIR/credentials.txt" + + if [ "$DEPLOYMENT_MODE" = "standalone" ]; then + cat > "$f" << EOF +=========================================== + CAMELEER SERVER CREDENTIALS + Generated: $(date -u '+%Y-%m-%d %H:%M:%S UTC') + + SECURE THIS FILE AND DELETE AFTER NOTING + THESE CREDENTIALS CANNOT BE RECOVERED +=========================================== + +Server Dashboard: ${PUBLIC_PROTOCOL}://${PUBLIC_HOST}/ +Admin User: ${ADMIN_USER} +Admin Password: ${ADMIN_PASS} + +PostgreSQL: cameleer / ${POSTGRES_PASSWORD} +ClickHouse: default / ${CLICKHOUSE_PASSWORD} +EOF + chmod 600 "$f" + log_info "Saved credentials to credentials.txt" + return + fi + cat > "$f" << EOF =========================================== CAMELEER PLATFORM CREDENTIALS @@ -1141,6 +1383,10 @@ EOF } generate_install_doc() { + if [ "$DEPLOYMENT_MODE" = "standalone" ]; then + generate_install_doc_standalone + return + fi local f="$INSTALL_DIR/INSTALL.md" local tls_desc="Self-signed (auto-generated)" [ "$TLS_MODE" = "custom" ] && tls_desc="Custom certificate" @@ -1288,28 +1534,172 @@ EOF log_info "Generated INSTALL.md" } +generate_install_doc_standalone() { + local f="$INSTALL_DIR/INSTALL.md" + local tls_desc="Self-signed (auto-generated)" + [ "$TLS_MODE" = "custom" ] && tls_desc="Custom certificate" + + cat > "$f" << EOF +# Cameleer Server — Installation Documentation + +## Installation Summary + +| | | +|---|---| +| **Version** | ${VERSION} | +| **Date** | $(date -u '+%Y-%m-%d %H:%M:%S UTC') | +| **Installer** | v${CAMELEER_INSTALLER_VERSION} | +| **Mode** | Standalone (single-tenant) | +| **Install Directory** | ${INSTALL_DIR} | +| **Hostname** | ${PUBLIC_HOST} | +| **TLS** | ${tls_desc} | + +## Service URLs + +- **Server Dashboard:** ${PUBLIC_PROTOCOL}://${PUBLIC_HOST}/ +- **API Endpoint:** ${PUBLIC_PROTOCOL}://${PUBLIC_HOST}/api/ + +## First Steps + +1. Open the Server Dashboard in your browser +2. Log in with the admin credentials from \`credentials.txt\` +3. Upload a Camel application JAR to deploy your first route +4. Monitor traces, metrics, and logs in the dashboard + +## Architecture + +| Container | Purpose | +|---|---| +| \`traefik\` | Reverse proxy, TLS termination, routing | +| \`postgres\` | PostgreSQL database (server data) | +| \`clickhouse\` | Time-series storage (traces, metrics, logs) | +| \`server\` | Cameleer Server (Spring Boot backend) | +| \`server-ui\` | Cameleer Dashboard (React frontend) | + +## Networking + +| Port | Service | +|---|---| +| ${HTTP_PORT} | HTTP (redirects to HTTPS) | +| ${HTTPS_PORT} | HTTPS (main entry point) | +EOF + + if [ -n "$MONITORING_NETWORK" ]; then + cat >> "$f" << EOF + +### Monitoring + +Services are connected to the \`${MONITORING_NETWORK}\` Docker network for Prometheus auto-discovery. +EOF + fi + + cat >> "$f" << EOF + +## TLS + +**Mode:** ${tls_desc} +EOF + + if [ "$TLS_MODE" = "self-signed" ]; then + cat >> "$f" << 'EOF' + +The platform generated a self-signed certificate on first boot. Replace it by +placing your certificate and key files in the `certs/` directory and restarting. +EOF + fi + + cat >> "$f" << EOF + +## Data & Backups + +| Docker Volume | Contains | +|---|---| +| \`pgdata\` | PostgreSQL data (server config, routes, deployments) | +| \`chdata\` | ClickHouse data (traces, metrics, logs) | +| \`certs\` | TLS certificates | +| \`jars\` | Uploaded application JARs | + +### Backup Commands + +\`\`\`bash +# PostgreSQL +docker compose -p ${COMPOSE_PROJECT} exec postgres pg_dump -U cameleer cameleer3 > backup.sql + +# ClickHouse +docker compose -p ${COMPOSE_PROJECT} exec clickhouse clickhouse-client --query "SELECT * FROM cameleer.traces FORMAT Native" > traces.native +\`\`\` + +## Upgrading + +Re-run the installer with a new version: + +\`\`\`bash +curl -sfL https://install.cameleer.io | bash -s -- --install-dir ${INSTALL_DIR} --version NEW_VERSION +\`\`\` + +The installer preserves your \`.env\`, credentials, and data volumes. Only the compose file and images are updated. + +## Troubleshooting + +| Issue | Command | +|---|---| +| Service not starting | \`docker compose -p ${COMPOSE_PROJECT} logs SERVICE_NAME\` | +| Server issues | \`docker compose -p ${COMPOSE_PROJECT} logs server\` | +| Routing issues | \`docker compose -p ${COMPOSE_PROJECT} logs traefik\` | +| Database issues | \`docker compose -p ${COMPOSE_PROJECT} exec postgres psql -U cameleer -d cameleer3\` | + +## Uninstalling + +\`\`\`bash +# Stop and remove containers +cd ${INSTALL_DIR} && docker compose -p ${COMPOSE_PROJECT} down + +# Remove data volumes (DESTRUCTIVE) +cd ${INSTALL_DIR} && docker compose -p ${COMPOSE_PROJECT} down -v + +# Remove install directory +rm -rf ${INSTALL_DIR} +\`\`\` +EOF + + log_info "Generated INSTALL.md" +} + print_credentials() { echo "" echo -e "${BOLD}==========================================${NC}" - echo -e "${BOLD} CAMELEER PLATFORM CREDENTIALS${NC}" + if [ "$DEPLOYMENT_MODE" = "standalone" ]; then + echo -e "${BOLD} CAMELEER SERVER CREDENTIALS${NC}" + else + echo -e "${BOLD} CAMELEER PLATFORM CREDENTIALS${NC}" + fi echo -e "${BOLD}==========================================${NC}" echo "" - echo -e " Admin Console: ${BLUE}${PUBLIC_PROTOCOL}://${PUBLIC_HOST}/platform/${NC}" + + if [ "$DEPLOYMENT_MODE" = "standalone" ]; then + echo -e " Dashboard: ${BLUE}${PUBLIC_PROTOCOL}://${PUBLIC_HOST}/${NC}" + else + echo -e " Admin Console: ${BLUE}${PUBLIC_PROTOCOL}://${PUBLIC_HOST}/platform/${NC}" + fi echo -e " Admin User: ${BOLD}${ADMIN_USER}${NC}" echo -e " Admin Password: ${BOLD}${ADMIN_PASS}${NC}" echo "" echo -e " PostgreSQL: cameleer / ${POSTGRES_PASSWORD}" echo -e " ClickHouse: default / ${CLICKHOUSE_PASSWORD}" echo "" - if [ "$VENDOR_ENABLED" = "true" ]; then - echo -e " Vendor User: ${BOLD}${VENDOR_USER}${NC}" - echo -e " Vendor Password: ${BOLD}${VENDOR_PASS}${NC}" - echo "" - fi - if [ "$LOGTO_CONSOLE_EXPOSED" = "true" ]; then - echo -e " Logto Console: ${BLUE}${PUBLIC_PROTOCOL}://${PUBLIC_HOST}:${LOGTO_CONSOLE_PORT}${NC}" - echo "" + + if [ "$DEPLOYMENT_MODE" = "saas" ]; then + if [ "$VENDOR_ENABLED" = "true" ]; then + echo -e " Vendor User: ${BOLD}${VENDOR_USER}${NC}" + echo -e " Vendor Password: ${BOLD}${VENDOR_PASS}${NC}" + echo "" + fi + if [ "$LOGTO_CONSOLE_EXPOSED" = "true" ]; then + echo -e " Logto Console: ${BLUE}${PUBLIC_PROTOCOL}://${PUBLIC_HOST}:${LOGTO_CONSOLE_PORT}${NC}" + echo "" + fi fi + echo -e " Credentials saved to: ${INSTALL_DIR}/credentials.txt" echo -e " ${YELLOW}Secure this file and delete after noting credentials.${NC}" echo "" @@ -1382,7 +1772,6 @@ handle_rerun() { docker_compose_down docker_compose_up verify_health - setup_single_tenant_record generate_install_doc print_summary exit 0 @@ -1476,9 +1865,6 @@ main() { # Verify health verify_health - # Create single-tenant record (after Flyway migrations have run) - setup_single_tenant_record - # Generate output files generate_credentials_file generate_install_doc