diff --git a/config.sh b/config.sh index dc4105b3..9f239936 100755 --- a/config.sh +++ b/config.sh @@ -877,9 +877,15 @@ configure_pangolin() { read -rp " Pangolin API key: " pang_key read -rp " Pangolin Organization ID: " pang_org + # The Pangolin endpoint (dashboard/Newt WebSocket URL) may differ from the API URL. + # For example: API at api.example.org vs dashboard at pangolin.example.org + read -rp " Pangolin Endpoint (dashboard/Newt URL) [default: https://pangolin.bnkserve.org]: " pang_endpoint + pang_endpoint=${pang_endpoint:-https://pangolin.bnkserve.org} + update_env_var "PANGOLIN_API_URL" "$pang_url" update_env_var "PANGOLIN_API_KEY" "$pang_key" update_env_var "PANGOLIN_ORG_ID" "$pang_org" + update_env_var "PANGOLIN_ENDPOINT" "$pang_endpoint" success "Pangolin API credentials saved" @@ -896,7 +902,7 @@ configure_pangolin() { local health_status health_status=$(curl -s -o /dev/null -w "%{http_code}" -m 10 \ -H "Authorization: Bearer $pang_key" \ - "$pang_url/" 2>/dev/null) || true + "$pang_url/org/$pang_org/sites" 2>/dev/null) || true if [[ "$health_status" != "200" ]]; then warn "Could not reach Pangolin API (HTTP $health_status)." @@ -919,8 +925,8 @@ configure_pangolin() { site_choice=${site_choice:-3} case "$site_choice" in - 1) pangolin_create_site "$pang_url" "$pang_key" "$pang_org" ;; - 2) pangolin_connect_site "$pang_url" "$pang_key" "$pang_org" ;; + 1) pangolin_create_site "$pang_url" "$pang_key" "$pang_org" "$pang_endpoint" ;; + 2) pangolin_connect_site "$pang_url" "$pang_key" "$pang_org" "$pang_endpoint" ;; *) info "Complete tunnel setup in the admin GUI at /app/pangolin after starting services." ;; @@ -943,8 +949,122 @@ pangolin_api() { "${body_args[@]}" 2>/dev/null } +pangolin_create_resources() { + local api_url=$1 api_key=$2 org_id=$3 site_id=$4 domain=$5 + + info "Creating Pangolin resources and targets for $domain..." + + # Look up the domain ID from registered domains + local domains_resp domain_id + domains_resp=$(pangolin_api GET "$api_url/org/$org_id/domains" "$api_key") + domain_id=$(echo "$domains_resp" | jq -r --arg d "$domain" \ + '.data.domains[]? | select(.baseDomain == $d) | .domainId' 2>/dev/null) + + if [[ -z "$domain_id" ]]; then + warn "Domain '$domain' not found in Pangolin. Register it first in the dashboard." + info "After registering the domain, run the sync from the admin GUI at /app/pangolin." + return + fi + + success "Found domain: $domain (ID: $domain_id)" + + # Resource definitions: subdomain|name + # Empty subdomain = root domain + local -a resource_defs=( + "app|Admin GUI" + "api|API Server" + "|Public Site" + "db|NocoDB" + "docs|Documentation" + "code|Code Server" + "n8n|Workflows" + "git|Gitea" + "home|Homepage" + "listmonk|Newsletter" + "qr|Mini QR" + "draw|Excalidraw" + "vault|Vaultwarden" + "mail|MailHog" + "chat|Rocket.Chat" + "events|Gancio Events" + "meet|Jitsi Meet" + "grafana|Grafana" + ) + + local created=0 skipped=0 failed=0 + + for def in "${resource_defs[@]}"; do + local subdomain="${def%%|*}" + local name="${def#*|}" + local full_domain + if [[ -n "$subdomain" ]]; then + full_domain="$subdomain.$domain" + else + full_domain="$domain" + fi + + # Build create payload — omit subdomain entirely for root domain (Pangolin rejects empty string) + local create_payload + if [[ -n "$subdomain" ]]; then + create_payload=$(jq -n \ + --arg name "$name" \ + --arg domainId "$domain_id" \ + --arg subdomain "$subdomain" \ + '{name: $name, domainId: $domainId, subdomain: $subdomain, http: true, protocol: "tcp"}') + else + create_payload=$(jq -n \ + --arg name "$name" \ + --arg domainId "$domain_id" \ + '{name: $name, domainId: $domainId, http: true, protocol: "tcp"}') + fi + + # Create the resource + local res_resp + res_resp=$(pangolin_api PUT "$api_url/org/$org_id/resource" "$api_key" "$create_payload") + + local resource_id + resource_id=$(echo "$res_resp" | jq -r '.data.resourceId // empty' 2>/dev/null) + + if [[ -z "$resource_id" ]]; then + local err_msg + err_msg=$(echo "$res_resp" | jq -r '.message // "unknown error"' 2>/dev/null) + if echo "$err_msg" | grep -qi "already exists\|duplicate\|conflict"; then + ((skipped++)) + else + warn " Failed to create $full_domain: $err_msg" + ((failed++)) + fi + continue + fi + + # Create target pointing to nginx:80 + local target_payload + target_payload=$(jq -n \ + --argjson siteId "$site_id" \ + '{siteId: $siteId, ip: "nginx", port: 80, method: "http", enabled: true}') + + pangolin_api PUT "$api_url/resource/$resource_id/target" "$api_key" "$target_payload" >/dev/null + + # Set resource as public (no SSO, no access block) + pangolin_api POST "$api_url/resource/$resource_id" "$api_key" \ + '{"sso":false,"blockAccess":false}' >/dev/null + + ((created++)) + done + + if [[ $created -gt 0 ]]; then + success "Created $created resources with targets → nginx:80" + fi + if [[ $skipped -gt 0 ]]; then + info "$skipped resources already existed (skipped)" + fi + if [[ $failed -gt 0 ]]; then + warn "$failed resources failed to create" + fi +} + pangolin_create_site() { - local api_url=$1 api_key=$2 org_id=$3 + local api_url=$1 api_key=$2 org_id=$3 endpoint=$4 local domain="${CONFIGURED_DOMAIN:-cmlite.org}" local site_name @@ -993,22 +1113,19 @@ pangolin_create_site() { success "Site created: $site_id ($site_name)" - # Derive endpoint from API URL (strip /v1 path) - local endpoint - endpoint=$(echo "$api_url" | sed 's|/v1/*$||') - # Write credentials to .env update_env_var "PANGOLIN_SITE_ID" "$site_id" update_env_var "PANGOLIN_NEWT_ID" "$newt_id" update_env_var "PANGOLIN_NEWT_SECRET" "$newt_secret" - update_env_var "PANGOLIN_ENDPOINT" "$endpoint" success "Tunnel credentials written to .env" - info "Resources will be created automatically via the admin GUI or sync endpoint." + + # Create resources and targets + pangolin_create_resources "$api_url" "$api_key" "$org_id" "$site_id" "$domain" } pangolin_connect_site() { - local api_url=$1 api_key=$2 org_id=$3 + local api_url=$1 api_key=$2 org_id=$3 endpoint=$4 info "Fetching sites from organization..." local sites_resp @@ -1066,37 +1183,50 @@ pangolin_connect_site() { sel_id=$(echo "$selected" | jq -r '.siteId') sel_name=$(echo "$selected" | jq -r '.name') - # Derive endpoint from API URL - local endpoint - endpoint=$(echo "$api_url" | sed 's|/v1/*$||') - # Write site ID to .env update_env_var "PANGOLIN_SITE_ID" "$sel_id" - update_env_var "PANGOLIN_ENDPOINT" "$endpoint" success "Connected to site: $sel_name (ID: $sel_id)" - # Check if we also need Newt credentials + # Fetch Newt credentials for this site from the API local existing_newt_id existing_newt_id=$(grep "^PANGOLIN_NEWT_ID=" "$ENV_FILE" 2>/dev/null | cut -d= -f2-) if [[ -z "$existing_newt_id" ]]; then - info "Newt credentials not yet set." - info "If you have the Newt ID and Secret for this site, enter them now." - info "Otherwise, set them later via the admin GUI." - echo "" - read -rp " Newt ID (leave blank to skip): " newt_id_input - if [[ -n "$newt_id_input" ]]; then - read -rp " Newt Secret: " newt_secret_input - if [[ -n "$newt_secret_input" ]]; then - update_env_var "PANGOLIN_NEWT_ID" "$newt_id_input" - update_env_var "PANGOLIN_NEWT_SECRET" "$newt_secret_input" - success "Newt credentials written to .env" + info "Fetching Newt credentials for this site..." + + # Try pickSiteDefaults for fresh Newt creds + local defaults_resp + defaults_resp=$(pangolin_api GET "$api_url/org/$org_id/pick-site-defaults" "$api_key") + local newt_id newt_secret + newt_id=$(echo "$defaults_resp" | jq -r '.data.newtId // .newtId // empty' 2>/dev/null) + newt_secret=$(echo "$defaults_resp" | jq -r '.data.newtSecret // .newtSecret // .data.secret // .secret // empty' 2>/dev/null) + + if [[ -n "$newt_id" && -n "$newt_secret" ]]; then + update_env_var "PANGOLIN_NEWT_ID" "$newt_id" + update_env_var "PANGOLIN_NEWT_SECRET" "$newt_secret" + success "Newt credentials fetched and saved (newtId: $newt_id)" + else + info "Could not auto-fetch Newt credentials." + info "Enter them manually (from your Pangolin dashboard)." + echo "" + read -rp " Newt ID (leave blank to skip): " newt_id_input + if [[ -n "$newt_id_input" ]]; then + read -rp " Newt Secret: " newt_secret_input + if [[ -n "$newt_secret_input" ]]; then + update_env_var "PANGOLIN_NEWT_ID" "$newt_id_input" + update_env_var "PANGOLIN_NEWT_SECRET" "$newt_secret_input" + success "Newt credentials written to .env" + fi fi fi else info "Existing Newt credentials found in .env (newtId: $existing_newt_id)" fi + + # Create resources and targets + local domain="${CONFIGURED_DOMAIN:-cmlite.org}" + pangolin_create_resources "$api_url" "$api_key" "$org_id" "$sel_id" "$domain" } configure_control_panel() { diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index ca951ade..bbba337e 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -914,7 +914,8 @@ services: image: ${GITEA_REGISTRY:-gitea.bnkops.com/admin}/mongo:6.0 container_name: mongodb-rocketchat restart: unless-stopped - entrypoint: ["/bin/bash", "-c", "if [ ! -f /data/replica.key ]; then openssl rand -base64 756 > /data/replica.key; fi && chmod 400 /data/replica.key && chown 999:999 /data/replica.key && exec mongod --replSet rs0 --bind_ip_all --auth --keyFile /data/replica.key"] + # Generate keyfile then delegate to Docker's standard entrypoint (creates INITDB user) + entrypoint: ["/bin/bash", "-c", "if [ ! -f /data/replica.key ]; then openssl rand -base64 756 > /data/replica.key; fi && chmod 400 /data/replica.key && chown 999:999 /data/replica.key && exec docker-entrypoint.sh mongod --replSet rs0 --bind_ip_all --keyFile /data/replica.key"] environment: MONGO_INITDB_ROOT_USERNAME: ${MONGO_ROOT_USER:-rocketchat} MONGO_INITDB_ROOT_PASSWORD: ${MONGO_ROOT_PASSWORD:?MONGO_ROOT_PASSWORD must be set in .env} diff --git a/docker-compose.yml b/docker-compose.yml index 4f157a5f..cdab741d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -940,7 +940,8 @@ services: image: mongo:6.0 container_name: mongodb-rocketchat restart: unless-stopped - entrypoint: ["/bin/bash", "-c", "if [ ! -f /data/replica.key ]; then openssl rand -base64 756 > /data/replica.key; fi && chmod 400 /data/replica.key && chown 999:999 /data/replica.key && exec mongod --replSet rs0 --bind_ip_all --auth --keyFile /data/replica.key"] + # Generate keyfile then delegate to Docker's standard entrypoint (creates INITDB user) + entrypoint: ["/bin/bash", "-c", "if [ ! -f /data/replica.key ]; then openssl rand -base64 756 > /data/replica.key; fi && chmod 400 /data/replica.key && chown 999:999 /data/replica.key && exec docker-entrypoint.sh mongod --replSet rs0 --bind_ip_all --keyFile /data/replica.key"] environment: MONGO_INITDB_ROOT_USERNAME: ${MONGO_ROOT_USER:-rocketchat} MONGO_INITDB_ROOT_PASSWORD: ${MONGO_ROOT_PASSWORD:?MONGO_ROOT_PASSWORD must be set in .env}