feat: standalone single-tenant deployment mode
Single-tenant installations now run the server directly without Logto or the SaaS management plane. The installer generates a simpler compose with 5 services: traefik, postgres, clickhouse, cameleer3-server, and cameleer3-server-ui. Uses local auth (built-in admin), no OIDC. Multi-tenant (vendor) mode is unchanged — full SaaS stack with Logto. Changes: - New DEPLOYMENT_MODE variable (standalone/saas) replaces TENANT_ORG_NAME - generate_compose_file_standalone() for the 5-service compose - Standalone traefik-dynamic.yml (no /platform/ redirect) - Stock postgres:16-alpine (server creates schema via Flyway) - Standalone health checks (server + UI instead of Logto + SaaS) - Standalone credentials/docs generation - Remove Phase 12b from bootstrap (no longer needed) - Remove setup_single_tenant_record (no longer needed) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -80,7 +80,6 @@ services:
|
|||||||
VENDOR_SEED_ENABLED: "${VENDOR_SEED_ENABLED:-false}"
|
VENDOR_SEED_ENABLED: "${VENDOR_SEED_ENABLED:-false}"
|
||||||
VENDOR_USER: ${VENDOR_USER:-vendor}
|
VENDOR_USER: ${VENDOR_USER:-vendor}
|
||||||
VENDOR_PASS: ${VENDOR_PASS:-vendor}
|
VENDOR_PASS: ${VENDOR_PASS:-vendor}
|
||||||
TENANT_ORG_NAME: ${TENANT_ORG_NAME:-}
|
|
||||||
healthcheck:
|
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"]
|
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
|
interval: 10s
|
||||||
|
|||||||
@@ -583,11 +583,9 @@ EOF
|
|||||||
chmod 644 "$BOOTSTRAP_FILE"
|
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
|
if [ "$VENDOR_SEED_ENABLED" = "true" ]; then
|
||||||
log ""
|
log ""
|
||||||
log "=== Phase 12a: Vendor Seed ==="
|
log "=== Phase 12a: Vendor Seed ==="
|
||||||
@@ -688,53 +686,6 @@ if [ "$VENDOR_SEED_ENABLED" = "true" ]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
log "Vendor seed complete."
|
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
|
fi
|
||||||
|
|
||||||
log ""
|
log ""
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ DEFAULT_LOGTO_CONSOLE_EXPOSED="true"
|
|||||||
DEFAULT_VENDOR_ENABLED="false"
|
DEFAULT_VENDOR_ENABLED="false"
|
||||||
DEFAULT_VENDOR_USER="vendor"
|
DEFAULT_VENDOR_USER="vendor"
|
||||||
DEFAULT_COMPOSE_PROJECT="cameleer-saas"
|
DEFAULT_COMPOSE_PROJECT="cameleer-saas"
|
||||||
|
DEFAULT_COMPOSE_PROJECT_STANDALONE="cameleer"
|
||||||
DEFAULT_DOCKER_SOCKET="/var/run/docker.sock"
|
DEFAULT_DOCKER_SOCKET="/var/run/docker.sock"
|
||||||
|
|
||||||
# --- Config values (set by args/env/config/prompts) ---
|
# --- Config values (set by args/env/config/prompts) ---
|
||||||
@@ -48,6 +49,7 @@ _ENV_MONITORING_NETWORK="${MONITORING_NETWORK:-}"
|
|||||||
_ENV_COMPOSE_PROJECT="${COMPOSE_PROJECT:-}"
|
_ENV_COMPOSE_PROJECT="${COMPOSE_PROJECT:-}"
|
||||||
_ENV_DOCKER_SOCKET="${DOCKER_SOCKET:-}"
|
_ENV_DOCKER_SOCKET="${DOCKER_SOCKET:-}"
|
||||||
_ENV_NODE_TLS_REJECT="${NODE_TLS_REJECT:-}"
|
_ENV_NODE_TLS_REJECT="${NODE_TLS_REJECT:-}"
|
||||||
|
_ENV_DEPLOYMENT_MODE="${DEPLOYMENT_MODE:-}"
|
||||||
|
|
||||||
INSTALL_DIR=""
|
INSTALL_DIR=""
|
||||||
PUBLIC_HOST=""
|
PUBLIC_HOST=""
|
||||||
@@ -72,7 +74,7 @@ VERSION=""
|
|||||||
COMPOSE_PROJECT=""
|
COMPOSE_PROJECT=""
|
||||||
DOCKER_SOCKET=""
|
DOCKER_SOCKET=""
|
||||||
NODE_TLS_REJECT=""
|
NODE_TLS_REJECT=""
|
||||||
TENANT_ORG_NAME=""
|
DEPLOYMENT_MODE=""
|
||||||
|
|
||||||
# --- State ---
|
# --- State ---
|
||||||
MODE="" # simple, expert, silent
|
MODE="" # simple, expert, silent
|
||||||
@@ -175,7 +177,9 @@ parse_args() {
|
|||||||
--compose-project) COMPOSE_PROJECT="$2"; shift ;;
|
--compose-project) COMPOSE_PROJECT="$2"; shift ;;
|
||||||
--docker-socket) DOCKER_SOCKET="$2"; shift ;;
|
--docker-socket) DOCKER_SOCKET="$2"; shift ;;
|
||||||
--node-tls-reject) NODE_TLS_REJECT="$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" ;;
|
--reconfigure) RERUN_ACTION="reconfigure" ;;
|
||||||
--reinstall) RERUN_ACTION="reinstall" ;;
|
--reinstall) RERUN_ACTION="reinstall" ;;
|
||||||
--confirm-destroy) CONFIRM_DESTROY=true ;;
|
--confirm-destroy) CONFIRM_DESTROY=true ;;
|
||||||
@@ -260,7 +264,7 @@ load_config_file() {
|
|||||||
compose_project) [ -z "$COMPOSE_PROJECT" ] && COMPOSE_PROJECT="$value" ;;
|
compose_project) [ -z "$COMPOSE_PROJECT" ] && COMPOSE_PROJECT="$value" ;;
|
||||||
docker_socket) [ -z "$DOCKER_SOCKET" ] && DOCKER_SOCKET="$value" ;;
|
docker_socket) [ -z "$DOCKER_SOCKET" ] && DOCKER_SOCKET="$value" ;;
|
||||||
node_tls_reject) [ -z "$NODE_TLS_REJECT" ] && NODE_TLS_REJECT="$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
|
esac
|
||||||
done < "$file"
|
done < "$file"
|
||||||
}
|
}
|
||||||
@@ -289,6 +293,7 @@ load_env_overrides() {
|
|||||||
[ -z "$COMPOSE_PROJECT" ] && COMPOSE_PROJECT="$_ENV_COMPOSE_PROJECT"
|
[ -z "$COMPOSE_PROJECT" ] && COMPOSE_PROJECT="$_ENV_COMPOSE_PROJECT"
|
||||||
[ -z "$DOCKER_SOCKET" ] && DOCKER_SOCKET="$_ENV_DOCKER_SOCKET"
|
[ -z "$DOCKER_SOCKET" ] && DOCKER_SOCKET="$_ENV_DOCKER_SOCKET"
|
||||||
[ -z "$NODE_TLS_REJECT" ] && NODE_TLS_REJECT="$_ENV_NODE_TLS_REJECT"
|
[ -z "$NODE_TLS_REJECT" ] && NODE_TLS_REJECT="$_ENV_NODE_TLS_REJECT"
|
||||||
|
[ -z "$DEPLOYMENT_MODE" ] && DEPLOYMENT_MODE="$_ENV_DEPLOYMENT_MODE"
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- Prerequisites ---
|
# --- Prerequisites ---
|
||||||
@@ -329,7 +334,9 @@ check_prerequisites() {
|
|||||||
|
|
||||||
check_port_available "${HTTP_PORT:-$DEFAULT_HTTP_PORT}" "HTTP"
|
check_port_available "${HTTP_PORT:-$DEFAULT_HTTP_PORT}" "HTTP"
|
||||||
check_port_available "${HTTPS_PORT:-$DEFAULT_HTTPS_PORT}" "HTTPS"
|
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
|
if [ $errors -gt 0 ]; then
|
||||||
log_error "$errors prerequisite(s) not met. Please install missing dependencies and retry."
|
log_error "$errors prerequisite(s) not met. Please install missing dependencies and retry."
|
||||||
@@ -430,19 +437,19 @@ run_simple_prompts() {
|
|||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo " Deployment mode:"
|
echo " Deployment mode:"
|
||||||
echo " [1] Multi-tenant vendor — admin manages platform, creates tenants on demand"
|
echo " [1] Multi-tenant vendor — manage platform, provision tenants on demand"
|
||||||
echo " [2] Single tenant — set up one tenant for immediate use"
|
echo " [2] Single-tenant — one server instance, local auth, no identity provider"
|
||||||
echo ""
|
echo ""
|
||||||
local deploy_choice
|
local deploy_choice
|
||||||
read -rp " Select mode [1]: " deploy_choice
|
read -rp " Select mode [1]: " deploy_choice
|
||||||
case "${deploy_choice:-1}" in
|
case "${deploy_choice:-1}" in
|
||||||
2)
|
2)
|
||||||
|
DEPLOYMENT_MODE="standalone"
|
||||||
VENDOR_ENABLED="false"
|
VENDOR_ENABLED="false"
|
||||||
prompt TENANT_ORG_NAME "Organization / tenant name" ""
|
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
|
DEPLOYMENT_MODE="saas"
|
||||||
VENDOR_ENABLED="true"
|
VENDOR_ENABLED="true"
|
||||||
TENANT_ORG_NAME=""
|
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
@@ -463,24 +470,28 @@ run_expert_prompts() {
|
|||||||
prompt_password CLICKHOUSE_PASSWORD "ClickHouse password" ""
|
prompt_password CLICKHOUSE_PASSWORD "ClickHouse password" ""
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo ""
|
if [ "$DEPLOYMENT_MODE" = "saas" ]; then
|
||||||
if prompt_yesno "Enable vendor account?"; then
|
echo ""
|
||||||
VENDOR_ENABLED="true"
|
if prompt_yesno "Enable vendor account?"; then
|
||||||
prompt VENDOR_USER "Vendor username" "${VENDOR_USER:-$DEFAULT_VENDOR_USER}"
|
VENDOR_ENABLED="true"
|
||||||
if prompt_yesno "Auto-generate vendor password?" "y"; then
|
prompt VENDOR_USER "Vendor username" "${VENDOR_USER:-$DEFAULT_VENDOR_USER}"
|
||||||
VENDOR_PASS=""
|
if prompt_yesno "Auto-generate vendor password?" "y"; then
|
||||||
|
VENDOR_PASS=""
|
||||||
|
else
|
||||||
|
prompt_password VENDOR_PASS "Vendor password" ""
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
prompt_password VENDOR_PASS "Vendor password" ""
|
VENDOR_ENABLED="false"
|
||||||
fi
|
fi
|
||||||
else
|
|
||||||
VENDOR_ENABLED="false"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${BOLD} Networking:${NC}"
|
echo -e "${BOLD} Networking:${NC}"
|
||||||
prompt HTTP_PORT "HTTP port" "${HTTP_PORT:-$DEFAULT_HTTP_PORT}"
|
prompt HTTP_PORT "HTTP port" "${HTTP_PORT:-$DEFAULT_HTTP_PORT}"
|
||||||
prompt HTTPS_PORT "HTTPS port" "${HTTPS_PORT:-$DEFAULT_HTTPS_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 ""
|
||||||
echo -e "${BOLD} Docker:${NC}"
|
echo -e "${BOLD} Docker:${NC}"
|
||||||
@@ -488,18 +499,21 @@ run_expert_prompts() {
|
|||||||
prompt COMPOSE_PROJECT "Compose project name" "${COMPOSE_PROJECT:-$DEFAULT_COMPOSE_PROJECT}"
|
prompt COMPOSE_PROJECT "Compose project name" "${COMPOSE_PROJECT:-$DEFAULT_COMPOSE_PROJECT}"
|
||||||
prompt DOCKER_SOCKET "Docker socket path" "${DOCKER_SOCKET:-$DEFAULT_DOCKER_SOCKET}"
|
prompt DOCKER_SOCKET "Docker socket path" "${DOCKER_SOCKET:-$DEFAULT_DOCKER_SOCKET}"
|
||||||
|
|
||||||
echo ""
|
if [ "$DEPLOYMENT_MODE" = "saas" ]; then
|
||||||
echo -e "${BOLD} Logto:${NC}"
|
echo ""
|
||||||
if prompt_yesno "Expose Logto admin console externally?" "y"; then
|
echo -e "${BOLD} Logto:${NC}"
|
||||||
LOGTO_CONSOLE_EXPOSED="true"
|
if prompt_yesno "Expose Logto admin console externally?" "y"; then
|
||||||
else
|
LOGTO_CONSOLE_EXPOSED="true"
|
||||||
LOGTO_CONSOLE_EXPOSED="false"
|
else
|
||||||
|
LOGTO_CONSOLE_EXPOSED="false"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- Config merge and validation ---
|
# --- Config merge and validation ---
|
||||||
|
|
||||||
merge_config() {
|
merge_config() {
|
||||||
|
: "${DEPLOYMENT_MODE:=saas}"
|
||||||
: "${INSTALL_DIR:=$DEFAULT_INSTALL_DIR}"
|
: "${INSTALL_DIR:=$DEFAULT_INSTALL_DIR}"
|
||||||
: "${PUBLIC_HOST:=localhost}"
|
: "${PUBLIC_HOST:=localhost}"
|
||||||
: "${PUBLIC_PROTOCOL:=$DEFAULT_PUBLIC_PROTOCOL}"
|
: "${PUBLIC_PROTOCOL:=$DEFAULT_PUBLIC_PROTOCOL}"
|
||||||
@@ -512,17 +526,24 @@ merge_config() {
|
|||||||
: "${VENDOR_ENABLED:=$DEFAULT_VENDOR_ENABLED}"
|
: "${VENDOR_ENABLED:=$DEFAULT_VENDOR_ENABLED}"
|
||||||
: "${VENDOR_USER:=$DEFAULT_VENDOR_USER}"
|
: "${VENDOR_USER:=$DEFAULT_VENDOR_USER}"
|
||||||
: "${VERSION:=$CAMELEER_DEFAULT_VERSION}"
|
: "${VERSION:=$CAMELEER_DEFAULT_VERSION}"
|
||||||
: "${COMPOSE_PROJECT:=$DEFAULT_COMPOSE_PROJECT}"
|
|
||||||
: "${DOCKER_SOCKET:=$DEFAULT_DOCKER_SOCKET}"
|
: "${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
|
# Force lowercase hostname — Logto normalizes internally, case mismatch breaks JWT validation
|
||||||
PUBLIC_HOST=$(echo "$PUBLIC_HOST" | tr '[:upper:]' '[:lower:]')
|
PUBLIC_HOST=$(echo "$PUBLIC_HOST" | tr '[:upper:]' '[:lower:]')
|
||||||
|
|
||||||
if [ -z "$NODE_TLS_REJECT" ]; then
|
if [ "$DEPLOYMENT_MODE" != "standalone" ]; then
|
||||||
if [ "$TLS_MODE" = "custom" ]; then
|
if [ -z "$NODE_TLS_REJECT" ]; then
|
||||||
NODE_TLS_REJECT="1"
|
if [ "$TLS_MODE" = "custom" ]; then
|
||||||
else
|
NODE_TLS_REJECT="1"
|
||||||
NODE_TLS_REJECT="0"
|
else
|
||||||
|
NODE_TLS_REJECT="0"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
@@ -545,7 +566,9 @@ validate_config() {
|
|||||||
fi
|
fi
|
||||||
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
|
local port_val
|
||||||
eval "port_val=\$$port_var"
|
eval "port_val=\$$port_var"
|
||||||
if ! echo "$port_val" | grep -qE '^[0-9]+$' || [ "$port_val" -lt 1 ] || [ "$port_val" -gt 65535 ]; then
|
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() {
|
generate_env_file() {
|
||||||
local f="$INSTALL_DIR/.env"
|
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
|
cat > "$f" << EOF
|
||||||
# Cameleer SaaS Configuration
|
# Cameleer SaaS Configuration
|
||||||
# Generated by installer v${CAMELEER_INSTALLER_VERSION} on $(date -u '+%Y-%m-%d %H:%M:%S UTC')
|
# 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_USER=${VENDOR_USER}
|
||||||
VENDOR_PASS=${VENDOR_PASS:-}
|
VENDOR_PASS=${VENDOR_PASS:-}
|
||||||
|
|
||||||
# Single-tenant org (when vendor is disabled)
|
|
||||||
TENANT_ORG_NAME=${TENANT_ORG_NAME:-}
|
|
||||||
|
|
||||||
# Docker
|
# Docker
|
||||||
DOCKER_SOCKET=${DOCKER_SOCKET}
|
DOCKER_SOCKET=${DOCKER_SOCKET}
|
||||||
DOCKER_GID=$(stat -c '%g' "${DOCKER_SOCKET}" 2>/dev/null || echo "0")
|
DOCKER_GID=$(stat -c '%g' "${DOCKER_SOCKET}" 2>/dev/null || echo "0")
|
||||||
@@ -661,6 +719,10 @@ EOF
|
|||||||
}
|
}
|
||||||
|
|
||||||
generate_compose_file() {
|
generate_compose_file() {
|
||||||
|
if [ "$DEPLOYMENT_MODE" = "standalone" ]; then
|
||||||
|
generate_compose_file_standalone
|
||||||
|
return
|
||||||
|
fi
|
||||||
local f="$INSTALL_DIR/docker-compose.yml"
|
local f="$INSTALL_DIR/docker-compose.yml"
|
||||||
: > "$f"
|
: > "$f"
|
||||||
|
|
||||||
@@ -796,7 +858,6 @@ EOF
|
|||||||
VENDOR_SEED_ENABLED: "${VENDOR_SEED_ENABLED:-false}"
|
VENDOR_SEED_ENABLED: "${VENDOR_SEED_ENABLED:-false}"
|
||||||
VENDOR_USER: ${VENDOR_USER:-vendor}
|
VENDOR_USER: ${VENDOR_USER:-vendor}
|
||||||
VENDOR_PASS: ${VENDOR_PASS:-vendor}
|
VENDOR_PASS: ${VENDOR_PASS:-vendor}
|
||||||
TENANT_ORG_NAME: ${TENANT_ORG_NAME:-}
|
|
||||||
healthcheck:
|
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"]
|
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
|
interval: 10s
|
||||||
@@ -920,6 +981,208 @@ EOF
|
|||||||
log_info "Generated docker-compose.yml"
|
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 operations ---
|
||||||
|
|
||||||
docker_compose_pull() {
|
docker_compose_pull() {
|
||||||
@@ -1001,14 +1264,22 @@ verify_health() {
|
|||||||
[ $failed -eq 0 ] && \
|
[ $failed -eq 0 ] && \
|
||||||
wait_for_docker_healthy "ClickHouse" "clickhouse" 120 || failed=1
|
wait_for_docker_healthy "ClickHouse" "clickhouse" 120 || failed=1
|
||||||
|
|
||||||
[ $failed -eq 0 ] && \
|
if [ "$DEPLOYMENT_MODE" = "standalone" ]; then
|
||||||
wait_for_docker_healthy "Logto + Bootstrap" "logto" 300 || failed=1
|
[ $failed -eq 0 ] && \
|
||||||
|
wait_for_docker_healthy "Cameleer Server" "server" 300 || failed=1
|
||||||
|
|
||||||
[ $failed -eq 0 ] && \
|
[ $failed -eq 0 ] && \
|
||||||
check_endpoint "Cameleer SaaS" "https://localhost:${HTTPS_PORT}/platform/api/config" 120 || failed=1
|
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 ] && \
|
[ $failed -eq 0 ] && \
|
||||||
check_endpoint "Traefik routing" "https://localhost:${HTTPS_PORT}/" 30 || failed=1
|
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 ""
|
echo ""
|
||||||
if [ $failed -ne 0 ]; then
|
if [ $failed -ne 0 ]; then
|
||||||
@@ -1018,58 +1289,6 @@ verify_health() {
|
|||||||
log_success "All services healthy."
|
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 ---
|
# --- Output file generation ---
|
||||||
|
|
||||||
write_config_file() {
|
write_config_file() {
|
||||||
@@ -1094,13 +1313,36 @@ version=${VERSION}
|
|||||||
compose_project=${COMPOSE_PROJECT}
|
compose_project=${COMPOSE_PROJECT}
|
||||||
docker_socket=${DOCKER_SOCKET}
|
docker_socket=${DOCKER_SOCKET}
|
||||||
node_tls_reject=${NODE_TLS_REJECT}
|
node_tls_reject=${NODE_TLS_REJECT}
|
||||||
tenant_org_name=${TENANT_ORG_NAME}
|
deployment_mode=${DEPLOYMENT_MODE}
|
||||||
EOF
|
EOF
|
||||||
log_info "Saved installer config to cameleer.conf"
|
log_info "Saved installer config to cameleer.conf"
|
||||||
}
|
}
|
||||||
|
|
||||||
generate_credentials_file() {
|
generate_credentials_file() {
|
||||||
local f="$INSTALL_DIR/credentials.txt"
|
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
|
cat > "$f" << EOF
|
||||||
===========================================
|
===========================================
|
||||||
CAMELEER PLATFORM CREDENTIALS
|
CAMELEER PLATFORM CREDENTIALS
|
||||||
@@ -1141,6 +1383,10 @@ EOF
|
|||||||
}
|
}
|
||||||
|
|
||||||
generate_install_doc() {
|
generate_install_doc() {
|
||||||
|
if [ "$DEPLOYMENT_MODE" = "standalone" ]; then
|
||||||
|
generate_install_doc_standalone
|
||||||
|
return
|
||||||
|
fi
|
||||||
local f="$INSTALL_DIR/INSTALL.md"
|
local f="$INSTALL_DIR/INSTALL.md"
|
||||||
local tls_desc="Self-signed (auto-generated)"
|
local tls_desc="Self-signed (auto-generated)"
|
||||||
[ "$TLS_MODE" = "custom" ] && tls_desc="Custom certificate"
|
[ "$TLS_MODE" = "custom" ] && tls_desc="Custom certificate"
|
||||||
@@ -1288,28 +1534,172 @@ EOF
|
|||||||
log_info "Generated INSTALL.md"
|
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() {
|
print_credentials() {
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${BOLD}==========================================${NC}"
|
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 -e "${BOLD}==========================================${NC}"
|
||||||
echo ""
|
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 User: ${BOLD}${ADMIN_USER}${NC}"
|
||||||
echo -e " Admin Password: ${BOLD}${ADMIN_PASS}${NC}"
|
echo -e " Admin Password: ${BOLD}${ADMIN_PASS}${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
echo -e " PostgreSQL: cameleer / ${POSTGRES_PASSWORD}"
|
echo -e " PostgreSQL: cameleer / ${POSTGRES_PASSWORD}"
|
||||||
echo -e " ClickHouse: default / ${CLICKHOUSE_PASSWORD}"
|
echo -e " ClickHouse: default / ${CLICKHOUSE_PASSWORD}"
|
||||||
echo ""
|
echo ""
|
||||||
if [ "$VENDOR_ENABLED" = "true" ]; then
|
|
||||||
echo -e " Vendor User: ${BOLD}${VENDOR_USER}${NC}"
|
if [ "$DEPLOYMENT_MODE" = "saas" ]; then
|
||||||
echo -e " Vendor Password: ${BOLD}${VENDOR_PASS}${NC}"
|
if [ "$VENDOR_ENABLED" = "true" ]; then
|
||||||
echo ""
|
echo -e " Vendor User: ${BOLD}${VENDOR_USER}${NC}"
|
||||||
fi
|
echo -e " Vendor Password: ${BOLD}${VENDOR_PASS}${NC}"
|
||||||
if [ "$LOGTO_CONSOLE_EXPOSED" = "true" ]; then
|
echo ""
|
||||||
echo -e " Logto Console: ${BLUE}${PUBLIC_PROTOCOL}://${PUBLIC_HOST}:${LOGTO_CONSOLE_PORT}${NC}"
|
fi
|
||||||
echo ""
|
if [ "$LOGTO_CONSOLE_EXPOSED" = "true" ]; then
|
||||||
|
echo -e " Logto Console: ${BLUE}${PUBLIC_PROTOCOL}://${PUBLIC_HOST}:${LOGTO_CONSOLE_PORT}${NC}"
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo -e " Credentials saved to: ${INSTALL_DIR}/credentials.txt"
|
echo -e " Credentials saved to: ${INSTALL_DIR}/credentials.txt"
|
||||||
echo -e " ${YELLOW}Secure this file and delete after noting credentials.${NC}"
|
echo -e " ${YELLOW}Secure this file and delete after noting credentials.${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
@@ -1382,7 +1772,6 @@ handle_rerun() {
|
|||||||
docker_compose_down
|
docker_compose_down
|
||||||
docker_compose_up
|
docker_compose_up
|
||||||
verify_health
|
verify_health
|
||||||
setup_single_tenant_record
|
|
||||||
generate_install_doc
|
generate_install_doc
|
||||||
print_summary
|
print_summary
|
||||||
exit 0
|
exit 0
|
||||||
@@ -1476,9 +1865,6 @@ main() {
|
|||||||
# Verify health
|
# Verify health
|
||||||
verify_health
|
verify_health
|
||||||
|
|
||||||
# Create single-tenant record (after Flyway migrations have run)
|
|
||||||
setup_single_tenant_record
|
|
||||||
|
|
||||||
# Generate output files
|
# Generate output files
|
||||||
generate_credentials_file
|
generate_credentials_file
|
||||||
generate_install_doc
|
generate_install_doc
|
||||||
|
|||||||
Reference in New Issue
Block a user