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 v2 (read-only data browser) ---
|
||||||
# NocoDB uses its own database (nocodb_meta) to avoid conflicts with Prisma
|
# 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
|
# 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_V2_PORT=8091
|
||||||
NOCODB_URL=http://changemaker-v2-nocodb:8080
|
NOCODB_URL=http://changemaker-v2-nocodb:8080
|
||||||
NOCODB_PORT=8091
|
NOCODB_PORT=8091
|
||||||
|
|||||||
@ -290,6 +290,31 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- changemaker-lite
|
- 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)
|
# 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