Add nocodb-init container for automatic database registration
Follows the listmonk-init pattern: an alpine:3 container that runs once after NocoDB is healthy, calls the REST API to register changemaker_v2 as a browsable data source, and exits. Idempotent — exits immediately if the base already has tables, and guards against duplicate sources during async table discovery. Bunker Admin
This commit is contained in:
parent
46d7912854
commit
a37d9910af
@ -106,6 +106,7 @@ REPRESENT_API_URL=https://represent.opennorth.ca
|
||||
# --- NocoDB v2 (read-only data browser) ---
|
||||
# NocoDB uses its own database (nocodb_meta) to avoid conflicts with Prisma
|
||||
# The database is auto-created by init-nocodb-db.sh on first PostgreSQL startup
|
||||
# nocodb-init container auto-registers changemaker_v2 as a browsable data source
|
||||
NOCODB_V2_PORT=8091
|
||||
NOCODB_URL=http://changemaker-v2-nocodb:8080
|
||||
NOCODB_PORT=8091
|
||||
|
||||
@ -290,6 +290,31 @@ services:
|
||||
networks:
|
||||
- changemaker-lite
|
||||
|
||||
# NocoDB Init — auto-registers changemaker_v2 as a browsable data source
|
||||
nocodb-init:
|
||||
image: alpine:3
|
||||
container_name: nocodb-init
|
||||
depends_on:
|
||||
nocodb-v2:
|
||||
condition: service_healthy
|
||||
v2-postgres:
|
||||
condition: service_healthy
|
||||
restart: "no"
|
||||
environment:
|
||||
NOCODB_URL: http://changemaker-v2-nocodb:8080
|
||||
NC_ADMIN_EMAIL: ${NC_ADMIN_EMAIL:-admin@cmlite.org}
|
||||
NC_ADMIN_PASSWORD: ${NC_ADMIN_PASSWORD:?NC_ADMIN_PASSWORD must be set in .env}
|
||||
DB_HOST: changemaker-v2-postgres
|
||||
DB_PORT: "5432"
|
||||
DB_USER: ${V2_POSTGRES_USER:-changemaker}
|
||||
DB_PASSWORD: ${V2_POSTGRES_PASSWORD:-changemaker}
|
||||
DB_NAME: ${V2_POSTGRES_DB:-changemaker_v2}
|
||||
volumes:
|
||||
- ./scripts/nocodb-init.sh:/init.sh:ro
|
||||
entrypoint: ["/bin/sh", "/init.sh"]
|
||||
networks:
|
||||
- changemaker-lite
|
||||
|
||||
# =========================================================================
|
||||
# SHARED INFRASTRUCTURE (kept from v1)
|
||||
# =========================================================================
|
||||
|
||||
209
scripts/nocodb-init.sh
Executable file
209
scripts/nocodb-init.sh
Executable file
@ -0,0 +1,209 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
PREFIX="[nocodb-init]"
|
||||
|
||||
# ─── Utilities ────────────────────────────────────────────────────────────────
|
||||
|
||||
log() { echo "$PREFIX $1"; }
|
||||
fail() { echo "$PREFIX ERROR: $1" >&2; exit 1; }
|
||||
|
||||
api() {
|
||||
# api METHOD /path [data]
|
||||
METHOD="$1"; ENDPOINT="$2"; DATA="$3"
|
||||
if [ -n "$DATA" ]; then
|
||||
curl -sf -X "$METHOD" "${NOCODB_URL}${ENDPOINT}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "xc-auth: ${TOKEN}" \
|
||||
-d "$DATA"
|
||||
else
|
||||
curl -sf -X "$METHOD" "${NOCODB_URL}${ENDPOINT}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "xc-auth: ${TOKEN}"
|
||||
fi
|
||||
}
|
||||
|
||||
# ─── Install dependencies ─────────────────────────────────────────────────────
|
||||
|
||||
apk add --no-cache curl jq >/dev/null 2>&1
|
||||
log "Dependencies installed"
|
||||
|
||||
# ─── Step 1: Authenticate ─────────────────────────────────────────────────────
|
||||
|
||||
log "Authenticating with NocoDB..."
|
||||
TOKEN=""
|
||||
for i in $(seq 1 15); do
|
||||
RESPONSE=$(curl -sf -X POST "${NOCODB_URL}/api/v1/auth/user/signin" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"email\":\"${NC_ADMIN_EMAIL}\",\"password\":\"${NC_ADMIN_PASSWORD}\"}" 2>/dev/null) || true
|
||||
|
||||
if [ -n "$RESPONSE" ]; then
|
||||
TOKEN=$(echo "$RESPONSE" | jq -r '.token // empty')
|
||||
if [ -n "$TOKEN" ]; then
|
||||
break
|
||||
fi
|
||||
fi
|
||||
|
||||
log " Waiting for NocoDB to be ready... (attempt $i/15)"
|
||||
sleep 3
|
||||
done
|
||||
|
||||
if [ -z "$TOKEN" ]; then
|
||||
fail "Could not authenticate with NocoDB after 15 attempts"
|
||||
fi
|
||||
log "Authenticated"
|
||||
|
||||
# ─── Step 2: Check for existing base ──────────────────────────────────────────
|
||||
|
||||
BASE_NAME="Changemaker Platform"
|
||||
INTEGRATION_NAME="Changemaker PostgreSQL"
|
||||
|
||||
log "Checking for existing base..."
|
||||
BASES=$(api GET "/api/v2/meta/bases/")
|
||||
EXISTING_BASE_ID=$(echo "$BASES" | jq -r --arg name "$BASE_NAME" '.list[] | select(.title == $name) | .id // empty' 2>/dev/null)
|
||||
|
||||
if [ -n "$EXISTING_BASE_ID" ]; then
|
||||
# Base exists — check if it has tables (fully synced)
|
||||
TABLES=$(api GET "/api/v2/meta/bases/${EXISTING_BASE_ID}/tables")
|
||||
TABLE_COUNT=$(echo "$TABLES" | jq -r '.list | length' 2>/dev/null)
|
||||
|
||||
if [ "$TABLE_COUNT" -gt 0 ] 2>/dev/null; then
|
||||
log "Base '${BASE_NAME}' already exists with ${TABLE_COUNT} tables. Nothing to do."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Check if an integration-linked source already exists (sync may still be in progress)
|
||||
# NocoDB auto-creates a default source (fk_integration_id=null, is_local=true) — ignore it
|
||||
SOURCES=$(api GET "/api/v2/meta/bases/${EXISTING_BASE_ID}/sources" 2>/dev/null) || true
|
||||
HAS_REAL_SOURCE=$(echo "$SOURCES" | jq -r '[.list[] | select(.fk_integration_id != null)] | length' 2>/dev/null)
|
||||
|
||||
if [ "$HAS_REAL_SOURCE" -gt 0 ] 2>/dev/null; then
|
||||
log "Base has an integration source — table sync in progress, waiting..."
|
||||
for i in $(seq 1 30); do
|
||||
TABLES=$(api GET "/api/v2/meta/bases/${EXISTING_BASE_ID}/tables" 2>/dev/null) || true
|
||||
TABLE_COUNT=$(echo "$TABLES" | jq -r '.list | length' 2>/dev/null)
|
||||
if [ "$TABLE_COUNT" -gt 0 ] 2>/dev/null; then
|
||||
log "Table sync complete: ${TABLE_COUNT} tables discovered"
|
||||
exit 0
|
||||
fi
|
||||
sleep 3
|
||||
done
|
||||
log "Source exists, tables will appear eventually. Nothing more to do."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
log "Base exists but has no integration source — will add one"
|
||||
fi
|
||||
|
||||
# ─── Step 3: Create or reuse integration ──────────────────────────────────────
|
||||
|
||||
log "Checking for existing integration..."
|
||||
INTEGRATIONS=$(api GET "/api/v2/meta/integrations")
|
||||
INTEGRATION_ID=$(echo "$INTEGRATIONS" | jq -r --arg name "$INTEGRATION_NAME" '.list[] | select(.title == $name) | .id // empty' 2>/dev/null)
|
||||
|
||||
if [ -z "$INTEGRATION_ID" ]; then
|
||||
log "Creating integration '${INTEGRATION_NAME}'..."
|
||||
INTEGRATION_PAYLOAD=$(cat <<INTEOF
|
||||
{
|
||||
"title": "${INTEGRATION_NAME}",
|
||||
"type": "database",
|
||||
"sub_type": "pg",
|
||||
"config": {
|
||||
"client": "pg",
|
||||
"connection": {
|
||||
"host": "${DB_HOST}",
|
||||
"port": ${DB_PORT},
|
||||
"user": "${DB_USER}",
|
||||
"password": "${DB_PASSWORD}",
|
||||
"database": "${DB_NAME}"
|
||||
}
|
||||
}
|
||||
}
|
||||
INTEOF
|
||||
)
|
||||
INTEGRATION_RESULT=$(api POST "/api/v2/meta/integrations" "$INTEGRATION_PAYLOAD")
|
||||
INTEGRATION_ID=$(echo "$INTEGRATION_RESULT" | jq -r '.id // empty')
|
||||
|
||||
if [ -z "$INTEGRATION_ID" ]; then
|
||||
fail "Failed to create integration"
|
||||
fi
|
||||
log "Integration created: ${INTEGRATION_ID}"
|
||||
else
|
||||
log "Reusing existing integration: ${INTEGRATION_ID}"
|
||||
fi
|
||||
|
||||
# ─── Step 4: Create base + add source ────────────────────────────────────────
|
||||
# NocoDB v0.301: inline sources in POST /bases/ are ignored.
|
||||
# Must create base first, then add source via POST /bases/{id}/sources.
|
||||
|
||||
SOURCE_PAYLOAD=$(cat <<SRCEOF
|
||||
{
|
||||
"type": "pg",
|
||||
"fk_integration_id": "${INTEGRATION_ID}",
|
||||
"inflection_column": "none",
|
||||
"inflection_table": "none",
|
||||
"config": {
|
||||
"client": "pg",
|
||||
"connection": {
|
||||
"host": "${DB_HOST}",
|
||||
"port": ${DB_PORT},
|
||||
"user": "${DB_USER}",
|
||||
"password": "${DB_PASSWORD}",
|
||||
"database": "${DB_NAME}"
|
||||
}
|
||||
}
|
||||
}
|
||||
SRCEOF
|
||||
)
|
||||
|
||||
if [ -z "$EXISTING_BASE_ID" ]; then
|
||||
log "Creating base '${BASE_NAME}'..."
|
||||
BASE_RESULT=$(api POST "/api/v2/meta/bases/" "{\"title\":\"${BASE_NAME}\"}")
|
||||
BASE_ID=$(echo "$BASE_RESULT" | jq -r '.id // empty')
|
||||
|
||||
if [ -z "$BASE_ID" ]; then
|
||||
fail "Failed to create base"
|
||||
fi
|
||||
log "Base created: ${BASE_ID}"
|
||||
else
|
||||
BASE_ID="$EXISTING_BASE_ID"
|
||||
fi
|
||||
|
||||
log "Adding PostgreSQL source to base..."
|
||||
api POST "/api/v2/meta/bases/${BASE_ID}/sources" "$SOURCE_PAYLOAD" >/dev/null
|
||||
log "Source added — table discovery started"
|
||||
|
||||
# ─── Step 5: Wait for table sync ─────────────────────────────────────────────
|
||||
|
||||
log "Waiting for table discovery..."
|
||||
for i in $(seq 1 30); do
|
||||
TABLES=$(api GET "/api/v2/meta/bases/${BASE_ID}/tables" 2>/dev/null) || true
|
||||
TABLE_COUNT=$(echo "$TABLES" | jq -r '.list | length' 2>/dev/null)
|
||||
|
||||
if [ "$TABLE_COUNT" -gt 0 ] 2>/dev/null; then
|
||||
log "Table sync complete: ${TABLE_COUNT} tables discovered"
|
||||
break
|
||||
fi
|
||||
|
||||
if [ "$i" = "30" ]; then
|
||||
log "Table sync still in progress after 90s — tables will appear eventually"
|
||||
break
|
||||
fi
|
||||
|
||||
sleep 3
|
||||
done
|
||||
|
||||
# ─── Step 6: Clean up default empty base ──────────────────────────────────────
|
||||
|
||||
log "Checking for default empty base..."
|
||||
# Re-fetch bases list (may have changed since step 2)
|
||||
BASES=$(api GET "/api/v2/meta/bases/")
|
||||
DEFAULT_BASE_ID=$(echo "$BASES" | jq -r '.list[] | select(.title == "Base") | .id // empty' 2>/dev/null)
|
||||
|
||||
if [ -n "$DEFAULT_BASE_ID" ] && [ "$DEFAULT_BASE_ID" != "$BASE_ID" ]; then
|
||||
log "Deleting default empty base..."
|
||||
api DELETE "/api/v2/meta/bases/${DEFAULT_BASE_ID}" >/dev/null 2>&1 || true
|
||||
log "Default base cleaned up"
|
||||
fi
|
||||
|
||||
log "Done"
|
||||
Loading…
x
Reference in New Issue
Block a user